Skip to content

Peak & frame inspection

refine_template searches a local grid of orientation, defocus, and pixel-size offsets around each particle and keeps only the single best-scoring hypothesis. The inspect_peaks and frame_inspection programs run that same local search but skip the best-peak reduction, returning the full grid of local scores for every particle instead. This is useful for diagnosing why a particle refined the way it did — for example, visualizing how sharply peaked the correlation is around the refined orientation, or checking a particle's stability across individual movie frames before trusting its refined pose.

Same configuration schema as refine_template

Both PeakInspectionManager and FrameInspectionManager are subclasses of RefineTemplateManager and reuse its exact YAML schema — see the refine template program details for the meaning of particle_stack, defocus_refinement_config, orientation_refinement_config, pixel_size_refinement_config, and preprocessing_filters. Whatever grid those refinement configs define is the grid that gets scored and returned in full, rather than reduced to a single best value.

Output tensors can be very large

The full local score grid is (N, n_px, n_defocus, n_orient, H, W) in "cross_correlation" mode — this can get large quickly for big particle stacks or fine-grained refinement configs. Use "frc" output mode (below) for a much more compact result, and/or subset the particle stack in your config for large runs. There is currently no automatic chunking of the particle stack to manage memory.

Peak inspection (PeakInspectionManager)

PeakInspectionManager runs the refine-template backend without best-peak reduction, returning the score at every searched hypothesis for every particle.

A default config file is available here on the GitHub page — it is identical in structure to the refine template example config.

Running peak inspection

We provide an example script, Leopard-EM/programs/inspect_peaks/run_inspect_peaks.py, which loads a config, runs peak inspection, and saves the result to a self-describing .npz file. Edit the constants near the top of the script:

YAML_CONFIG_PATH = "/path/to/inspect_peaks_configuration.yaml"
OUTPUT_PATH = "/path/to/results_inspect_peaks.npz"
CORRELATION_BATCH_SIZE = 32  # lower if you run out of GPU memory
OUTPUT_MODE = "cross_correlation"  # or "frc"

Or drive it directly from Python:

from leopard_em.pydantic_models.managers import PeakInspectionManager

manager = PeakInspectionManager.from_yaml("/path/to/inspect_peaks_configuration.yaml")
output_path = manager.run_and_save_peak_inspection(
    output_path="/path/to/results_inspect_peaks.npz",
    correlation_batch_size=32,
    prefer_refined_angles=True,
    output_mode="cross_correlation",  # or "frc"
)

run_peak_inspection(...) is also available if you'd rather get the raw tensor (and, in "frc" mode, the frequency bins) back in memory without writing a file.

Output modes

  • "cross_correlation" (default) — returns the full local cross-correlation map for every hypothesis, shape (N, n_px, n_defocus, n_orient, H, W).
  • "frc" — returns local Fourier ring correlation spectra instead of full 2-D maps, shape (N, n_px, n_defocus, n_orient, n_freq).

See Data from peak & frame inspection for the full .npz file layout and how to load results back with leopard_em.analysis.load_inspection_result.

Per-frame peak inspection (FrameInspectionManager)

FrameInspectionManager extends PeakInspectionManager to score each movie frame independently, rather than the motion-corrected sum used by match_template/refine_template/inspect_peaks. This is useful for checking how a particle's local correlation landscape evolves across a movie's exposure — for example, to spot particles that only correlate strongly in a subset of frames.

Configuring the movie

Per-frame inspection requires movie_config.enabled: true, plus either a per-frame deformation field or an explicit per-particle shifts CSV to align each particle box to the correct position in every frame:

movie_config:
  enabled: true
  movie_path: /some/path/to/aligned_or_unaligned_movie.mrc
  # Provide exactly one of the following two:
  deformation_field_path: /some/path/to/deformation_grid.csv
  # particle_shifts_path: /some/path/to/particle_shifts.csv
  pre_exposure: 0.0
  fluence_per_frame: 1.0

particle_shifts_path, if provided, takes precedence over deformation_field_path. It should be a CSV with columns particle_index, frame, y_shift, x_shift.

A default config file is available here on the GitHub page.

Optional per-frame template dose weighting

Setting apply_template_dose_weighting=True applies cumulative electron-dose filtering to the (non-dose-weighted) template separately for each frame's exposure interval, using pre_exposure/fluence_per_frame from movie_config — this accounts for radiation damage accumulating over the course of the movie when scoring later frames. It is off by default.

Running per-frame peak inspection

We provide an example script, Leopard-EM/programs/inspect_peaks/run_frame_inspection.py, which mirrors run_inspect_peaks.py:

from leopard_em.pydantic_models.managers import FrameInspectionManager

manager = FrameInspectionManager.from_yaml("/path/to/frame_inspection_configuration.yaml")
output_path = manager.run_and_save_peak_inspection_per_frame(
    output_path="/path/to/results_frame_inspection.npz",
    correlation_batch_size=32,
    prefer_refined_angles=True,
    apply_projection_normalization=True,
    output_mode="cross_correlation",  # or "frc"
    apply_template_dose_weighting=False,
)

The resulting .npz carries an extra frame axis immediately after the particle axis — (N, T, n_px, n_defocus, n_orient, H, W) in "cross_correlation" mode, or (N, T, n_px, n_defocus, n_orient, n_freq) in "frc" mode, for T movie frames — and includes a frame_index array mapping that axis to actual movie frame numbers. See Data from peak & frame inspection for the complete layout.

Loading and inspecting results

Both programs write the same self-describing .npz format, loadable with a single helper regardless of which program produced it:

from leopard_em.analysis import load_inspection_result

result = load_inspection_result("results_inspect_peaks.npz")

result.scores                # main tensor; see result.axes for per-dimension labels
result.axes                  # e.g. ("particle", "pixel_size", "defocus", "orientation", "y", "x")
result.euler_angle_offsets   # (n_orient, 3) ZYZ offsets per orientation index
result.defocus_offsets       # (n_defocus,) relative defocus offsets (Angstroms)
result.pixel_size_offsets    # (n_px,) relative pixel-size offsets
result.particle_index        # (N,) maps tensor rows back to the particle stack, if available
result.frequency_bins        # (n_freq,) FRC frequencies, "frc" mode only
result.frame_index           # (T,) movie frame index, per-frame inspection only