Skip to content

cylindra_builtins.imod

This built-in plugin submodule provides functions to work with IMOD file formats.

export_project(ui, layer, save_dir, template_path, mask_params=None, project_name='project-0')

Export cylindra state as a PEET prm file.

Molecules and images will be exported to a directory that can be directly used by PEET.

Parameters:

Name Type Description Default
layer MoleculesLayer

Molecules layer to export.

required
save_dir Path

Directory to save the files needed for a PEET project.

required
template_path str

Path to the template image.

required
mask_params Any

Mask parameters.

None
project_name str

Name of the PEET project.

"project-0"
Source code in cylindra_builtins/imod/io.py
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
@register_function(name="Export project")
def export_project(
    ui: CylindraMainWidget,
    layer: MoleculesLayerType,
    save_dir: Path.Dir,
    template_path: Annotated[str, {"bind": _get_template_path}],
    mask_params: Annotated[Any, {"bind": _get_mask_params}] = None,
    project_name: str = "project-0",
):
    """Export cylindra state as a PEET prm file.

    Molecules and images will be exported to a directory that can be
    directly used by PEET.

    Parameters
    ----------
    layer : MoleculesLayer
        Molecules layer to export.
    save_dir : Path
        Directory to save the files needed for a PEET project.
    template_path : str
        Path to the template image.
    mask_params : Any, default None
        Mask parameters.
    project_name : str, default "project-0"
        Name of the PEET project.
    """
    save_dir = Path(save_dir)
    layer = assert_layer(layer, ui.parent_viewer)
    if not save_dir.exists():
        save_dir.mkdir()
    loader = ui.tomogram.get_subtomogram_loader(
        Molecules.empty(),
        binsize=1,
    )
    template_image, mask_image = loader.normalize_input(
        template=ui.sta.params._norm_template_param(template_path),
        mask=ui.sta.params._get_mask(params=mask_params),
    )

    if template_image is None:
        raise ValueError("Template image is not loaded.")

    # paths
    coordinates_path = "./coordinates.mod"
    angles_path = "./angles.csv"
    template_path = "./template-image.mrc"
    mask_path = "./mask-image.mrc"
    prm_path = save_dir / f"{project_name}.prm"

    txt = PEET_TEMPLATE.format(
        tomograms=repr(ui.tomogram.source),
        coordinates=repr(coordinates_path),
        angles=repr(angles_path),
        tilt_range=list(ui.tomogram.tilt["range"]),
        template=template_path,
        project_name=project_name,
        shape=list(template_image.shape),
        mask_type=mask_path,
    )

    # save files
    prm_path.write_text(txt)
    mol = layer.molecules
    _save_molecules(save_dir=save_dir, mol=mol, scale=ui.tomogram.scale)
    ip.asarray(template_image, axes="zyx").set_scale(
        zyx=ui.tomogram.scale, unit="nm"
    ).imsave(save_dir / template_path)
    if mask_image is not None:
        ip.asarray(mask_image, axes="zyx").set_scale(
            zyx=ui.tomogram.scale, unit="nm"
        ).imsave(save_dir / mask_path)

export_project_batch(ui, save_dir, path_sets, project_name='project-0', size=10.0)

Export cylindra batch analyzer state as a PEET prm file.

A epe file will be generated, which can directly be used by etomo <name>.epe.

Parameters:

Name Type Description Default
save_dir Path

Directory to save the files needed for a PEET project.

required
path_sets Any

Path sets of the tomograms and coordinates.

required
project_name str

Name of the PEET project.

"project-0"
size float

Size of the subtomograms in nanometers.

10.0
Source code in cylindra_builtins/imod/io.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
@register_function(name="Export project as batch")
def export_project_batch(
    ui: CylindraMainWidget,
    save_dir: Path.Dir,
    path_sets: Annotated[Any, {"bind": _get_loader_paths}],
    project_name: str = "project-0",
    size: Annotated[float, {"label": "Subtomogram size (nm)", "min": 1.0, "max": 1000.0}] = 10.0,  # fmt: skip
):
    """Export cylindra batch analyzer state as a PEET prm file.

    A epe file will be generated, which can directly be used by `etomo <name>.epe`.

    Parameters
    ----------
    save_dir : Path
        Directory to save the files needed for a PEET project.
    path_sets : Any
        Path sets of the tomograms and coordinates.
    project_name : str, default "project-0"
        Name of the PEET project.
    size : float, default 10.0
        Size of the subtomograms in nanometers.
    """
    from cylindra.widgets.batch._sequence import PathInfo
    from cylindra.widgets.batch._utils import TempFeatures

    save_dir = Path(save_dir)
    if not save_dir.exists():
        save_dir.mkdir()

    _temp_feat = TempFeatures()

    _tomogram_list = list[str]()
    _coords_list = list[str]()
    _angle_list = list[str]()
    _tilt_list = list[str]()
    _count = 0
    scales = []
    for path_info in path_sets:
        path_info = PathInfo(*path_info)
        prj = path_info.project_instance(missing_ok=False)
        moles = list(path_info.iter_molecules(_temp_feat, prj.scale))
        if len(moles) > 0:
            _tomogram_list.append(repr(path_info.image.as_posix()))
            mod_name = f"coordinates-{_count:0>3}_{path_info.image.stem}.mod"
            csv_name = f"angles-{_count:0>3}_{path_info.image.stem}.csv"
            _save_molecules(
                save_dir=save_dir,
                mol=Molecules.concat(moles),
                scale=prj.scale,
                mod_name=mod_name,
                csv_name=csv_name,
            )
            _coords_list.append(f"'./{mod_name}'")
            _angle_list.append(f"'./{csv_name}'")
            if mw_dict := prj.missing_wedge.as_param():
                if "range" in mw_dict:
                    tilt_range = mw_dict["range"]
                    _tilt_list.append(f"[{tilt_range[0]}, {tilt_range[1]}]")
            scales.append(prj.scale)
            _count += 1

    # determine shape using the average scale
    if len(scales) == 0:
        raise ValueError("No tomograms found in the project.")
    scale = np.mean(scales)
    shape = [int(round(size / scale / 2)) * 2] * 3  # must be even

    # paths
    prm_path = save_dir / f"{project_name}.prm"
    epe_path = save_dir / f"{project_name}.epe"

    prm_txt = PEET_TEMPLATE.format(
        tomograms=", ".join(_tomogram_list),
        coordinates=", ".join(_coords_list),
        angles=", ".join(_angle_list),
        tilt_range=", ".join(_tilt_list),
        template="",
        project_name=project_name,
        shape=shape,
        mask_type="none",
    )

    # save files
    prm_path.write_text(prm_txt)
    epe_path.write_text(f"Peet.RootName={project_name}\n")

import_imod_projects(ui, edf_path, project_root=None, scale_override=None, invert=True, bin_size=[1])

Import IMOD projects as batch analyzer entries.

Parameters:

Name Type Description Default
edf_path Path

Path to the edf file(s). Path can contain wildcards.

required
project_root Path

Root directory to save the cylindra project folders. If None, a new directory will be created under the same level as the first IMOD project.

None
scale_override float

Override the scale used for all the tomograms inside cylindra.

None
invert bool

If true, invert the intensity of the image.

False
bin_size list of int

Bin sizes to load the tomograms.

[1]
Source code in cylindra_builtins/imod/io.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
@register_function(name="Import IMOD projects")
def import_imod_projects(
    ui: CylindraMainWidget,
    edf_path: Annotated[Path.Read[FileFilter.EDF], {"label": "IMOD edf file(s)"}],
    project_root: Optional[Path.Save] = None,
    scale_override: Annotated[Optional[float], {"text": "Use header scale", "options": {"step": 0.0001, "value": 1.0}}] = None,
    invert: bool = True,
    bin_size: list[int] = [1],
):  # fmt: skip
    """Import IMOD projects as batch analyzer entries.

    Parameters
    ----------
    edf_path : Path
        Path to the edf file(s). Path can contain wildcards.
    project_root : Path, default None
        Root directory to save the cylindra project folders. If None, a new directory
        will be created under the same level as the first IMOD project.
    scale_override : float, default None
        Override the scale used for all the tomograms inside cylindra.
    invert : bool, default False
        If true, invert the intensity of the image.
    bin_size : list of int, default [1]
        Bin sizes to load the tomograms.
    """
    from cylindra.widgets.batch._utils import unwrap_wildcard

    tomo_paths: list[Path] = []
    tilt_models: list[dict | None] = []
    for each in unwrap_wildcard(edf_path):
        if (res := _edf_to_tomo_and_tilt(each)) is not None:
            tomo_path, tilt_model = res
            tomo_paths.append(tomo_path)
            tilt_models.append(tilt_model)
    if len(tomo_paths) == 0:
        raise ValueError(f"No tomograms found with the given path input: {edf_path}")
    if scale_override is not None:
        scales = [scale_override] * len(tomo_paths)
    else:
        scales = None

    if project_root is None:
        project_root = tomo_paths[0].parent.parent / "cylindra"

    ui.batch._new_projects_from_table(
        tomo_paths,
        save_root=project_root,
        scale=scales,
        tilt_model=tilt_models,
        invert=[invert] * len(tomo_paths),
        bin_size=[bin_size] * len(tomo_paths),
    )

load_molecules(ui, mod_path, ang_path, shift_mol=True)

Read molecule coordinates and angles from IMOD .mod files.

Parameters:

Name Type Description Default
mod_path Path

Path to the mod file that contains molecule coordinates.

required
ang_path Path

Path to the text file that contains molecule angles in Euler angles.

required
shift_mol bool

In PEET output csv there may be xOffset, yOffset, zOffset columns that can be directly applied to the molecule coordinates.

True
Source code in cylindra_builtins/imod/io.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@register_function(name="Load molecules")
def load_molecules(
    ui: CylindraMainWidget,
    mod_path: Annotated[Path.Read[FileFilter.MOD], {"label": "Path to MOD file"}],
    ang_path: Annotated[Path.Read[FileFilter.CSV], {"label": "Path to csv file"}],
    shift_mol: Annotated[bool, {"label": "Apply shifts to monomers if offsets are available."}] = True,
):  # fmt: skip
    """Read molecule coordinates and angles from IMOD .mod files.

    Parameters
    ----------
    mod_path : Path
        Path to the mod file that contains molecule coordinates.
    ang_path : Path
        Path to the text file that contains molecule angles in Euler angles.
    shift_mol : bool, default True
        In PEET output csv there may be xOffset, yOffset, zOffset columns that can
        be directly applied to the molecule coordinates.
    """
    mod_path = Path(mod_path)
    df = read_mod(mod_path)
    mod = df.select("z", "y", "x").to_numpy(writable=True)
    mod[:, 1:] -= 0.5  # shift to center of voxel
    shifts, angs = _read_shift_and_angle(ang_path)
    scale = ui.tomogram.scale
    mol = Molecules.from_euler(pos=mod * scale, angles=angs, degrees=True)
    if shift_mol:
        mol.translate(shifts * scale, copy=False)

    return add_molecules(ui.parent_viewer, mol, mod_path.name, source=None)

load_splines(ui, mod_path)

Read a mod file and register all the contours as splines.

Source code in cylindra_builtins/imod/io.py
56
57
58
59
60
61
62
63
64
65
66
@register_function(name="Load splines")
def load_splines(
    ui: CylindraMainWidget,
    mod_path: Annotated[Path.Read[FileFilter.MOD], {"label": "Path to MOD file"}],
):
    """Read a mod file and register all the contours as splines."""
    df = read_mod(mod_path)
    for _, sub in df.group_by("object_id", "contour_id", maintain_order=True):
        coords = sub.select("z", "y", "x").to_numpy(writable=True)
        coords[:, 1:] -= 0.5  # shift YX to center of voxel
        ui.register_path(coords * ui.tomogram.scale, err_max=1e-8)

open_image_from_imod_project(ui, edf_path, scale_override=None, bin_size=[4], filter=ImageFilter.Lowpass, invert=True, eager=False, cache_image=False)

Open an image from an IMOD project.

Parameters:

Name Type Description Default
edf_path Path

Path to the edf file.

required
scale_override float

Override the scale used for all the tomograms inside cylindra.

None
bin_size list of int

Bin sizes to load the tomograms.

[1]
filter ImageFilter

Filter to apply when binning the image.

ImageFilter.Lowpass
invert bool

If true, invert the intensity of the image.

False
eager bool

If true, the image will be loaded immediately. Otherwise, it will be loaded lazily.

False
cache_image bool

If true, the image will first be copied to the cache directory before loading.

False
Source code in cylindra_builtins/imod/io.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
@register_function(name="Open image from an IMOD project")
def open_image_from_imod_project(
    ui: CylindraMainWidget,
    edf_path: Annotated[Path.Read[FileFilter.EDF], {"label": "IMOD edf file"}],
    scale_override: Annotated[
        Optional[float],
        {"text": "Use header scale", "options": {"step": 0.0001, "value": 1.0}},
    ] = None,
    bin_size: list[int] = [4],
    filter: ImageFilter | None = ImageFilter.Lowpass,
    invert: bool = True,
    eager: Annotated[bool, {"label": "Load the entire image into memory"}] = False,
    cache_image: Annotated[bool, {"label": "Cache image on SSD"}] = False,
):
    """Open an image from an IMOD project.

    Parameters
    ----------
    edf_path : Path
        Path to the edf file.
    scale_override : float, default None
        Override the scale used for all the tomograms inside cylindra.
    bin_size : list of int, default [1]
        Bin sizes to load the tomograms.
    filter : ImageFilter, default ImageFilter.Lowpass
        Filter to apply when binning the image.
    invert : bool, default False
        If true, invert the intensity of the image.
    eager : bool, default False
        If true, the image will be loaded immediately. Otherwise, it will be loaded
        lazily.
    cache_image : bool, default False
        If true, the image will first be copied to the cache directory before
        loading.
    """
    res = _edf_to_tomo_and_tilt(edf_path)
    if res is None:
        raise ValueError(f"Could not find tomogram in the IMOD project {edf_path}.")
    tomo_path, tilt_model = res
    ui.open_image(
        tomo_path,
        scale=scale_override,
        invert=invert,
        tilt_range=tilt_model,
        bin_size=bin_size,
        filter=filter,
        eager=eager,
        cache_image=cache_image,
    )

save_molecules(ui, save_dir, layers)

Save monomer positions and angles in the PEET format.

Parameters:

Name Type Description Default
save_dir Path

Saving path.

required
layers sequence of MoleculesLayer

Select the layers to save. All the molecules will be concatenated.

required
Source code in cylindra_builtins/imod/io.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@register_function(name="Save molecules")
def save_molecules(
    ui: CylindraMainWidget, save_dir: Path.Dir, layers: MoleculesLayersType
):
    """Save monomer positions and angles in the PEET format.

    Parameters
    ----------
    save_dir : Path
        Saving path.
    layers : sequence of MoleculesLayer
        Select the layers to save. All the molecules will be concatenated.
    """
    save_dir = Path(save_dir)
    layers = assert_list_of_layers(layers, ui.parent_viewer)
    mol = Molecules.concat([l.molecules for l in layers])
    return _save_molecules(save_dir=save_dir, mol=mol, scale=ui.tomogram.scale)

save_splines(ui, save_path, interval=10.0)

Save splines as a mod file.

This function will sample coordinates along the splines and save the coordinates as a mod file. The mod file will be labeled with object_id=1 and contour_id=i+1, where i is the index of the spline.

Parameters:

Name Type Description Default
save_path Path

Saving path.

required
interval float

Sampling interval along the splines. For example, if interval=10.0 and the length of a spline is 100.0, 11 points will be sampled.

10.0
Source code in cylindra_builtins/imod/io.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
@register_function(name="Save splines")
def save_splines(
    ui: CylindraMainWidget,
    save_path: Path.Save[FileFilter.MOD],
    interval: Annotated[float, {"min": 0.01, "max": 1000.0, "label": "Sampling interval (nm)"}] = 10.0,
):  # fmt: skip
    """Save splines as a mod file.

    This function will sample coordinates along the splines and save the coordinates
    as a mod file. The mod file will be labeled with object_id=1 and contour_id=i+1,
    where i is the index of the spline.

    Parameters
    ----------
    save_path : Path
        Saving path.
    interval : float, default 10.0
        Sampling interval along the splines. For example, if interval=10.0 and the
        length of a spline is 100.0, 11 points will be sampled.
    """
    if interval <= 1e-4:
        raise ValueError("Interval must be larger than 1e-4.")
    data_list = []
    for i, spl in enumerate(ui.splines):
        num = int(spl.length() / interval)
        coords = spl.partition(num) / ui.tomogram.scale
        df = pl.DataFrame(
            {
                "object_id": 1,
                "contour_id": i + 1,
                "x": coords[:, 2] + 0.5,
                "y": coords[:, 1] + 0.5,
                "z": coords[:, 0],
            }
        )
        data_list.append(df)
    data_all = pl.concat(data_list, how="vertical")
    save_mod(save_path, data_all)