Source code for playnano.io.loader

"""
Common loader for various high speed AFM video formats *and* playNano export bundles.

Supported extensions:
  - .jpk        (folder)
  - .spm        (folder)
  - .h5-jpk     (single-file JPK)
  - .asd        (single-file ASD)
  - .ome.tif / .tif  (OME-TIFF bundles)
  - .npz        (playNano NPZ bundles)
  - .h5         (playNano HDF5 bundles)
"""

import logging
from pathlib import Path

from playnano.afm_stack import AFMImageStack
from playnano.io.data_loaders import (
    load_h5_bundle,
    load_npz_bundle,
    load_ome_tiff_stack,
)
from playnano.io.formats.read_asd import load_asd_file
from playnano.io.formats.read_h5jpk import load_h5jpk
from playnano.io.formats.read_jpk_folder import load_jpk_folder
from playnano.io.formats.read_spm_folder import load_spm_folder

logger = logging.getLogger(__name__)


[docs] def get_loader_for_folder( folder_path: Path, folder_loaders: dict ) -> tuple[str, callable]: """ Determine the appropriate loader for a folder containing AFM data. Parameters ---------- folder_path : Path Path to the folder. folder_loaders : dict Mapping from file extension to loader function. Returns ------- (str, callable) The chosen extension and loader function. Raises ------ FileNotFoundError If no known file types are found. """ logger.debug(f"Determining loader for folder {folder_path}") suffix_counts = {} for f in folder_path.iterdir(): if f.is_file(): ext = f.suffix.lower() # Handle 'old' nanoscope numeric extensions like .001, .002 as ".spm" if ext[1:].isdigit() and len(ext) == 4: ext = ".spm" if ext in folder_loaders: suffix_counts[ext] = suffix_counts.get(ext, 0) + 1 if not suffix_counts: raise FileNotFoundError("No supported AFM files found in the folder.") # Prefer .jpk for now chosen_ext = ( ".jpk" if ".jpk" in suffix_counts else max(suffix_counts, key=suffix_counts.get) ) return chosen_ext, folder_loaders[chosen_ext]
[docs] def get_loader_for_file( file_path: Path, file_loaders: dict, folder_loaders: dict ) -> callable: """ Determine the appropriate loader for a single multi-frame AFM file. Parameters ---------- file_path : Path Path to the file. file_loaders : dict Mapping from file extensions to file loader functions. folder_loaders : dict Mapping from extensions for folder loaders (for error handling). Returns ------- (str, callable) The file extention string and the loader function for the file. Raises ------ ValueError If the file type is unsupported or better handled as a folder. """ logger.debug(f"Determining loader for file {file_path}") ext = file_path.suffix.lower() if not ext: raise ValueError(f"{file_path} has no extension and cannot be identified.") if ext in file_loaders: return ext, file_loaders[ext] elif ext in folder_loaders: raise ValueError( f"The {ext} file type is typically a single-frame export." "To load HS-AFM video, pass the full folder instead." ) else: raise ValueError(f"Unsupported file type: {ext}")
[docs] def load_afm_stack(file_path: Path, channel: str = "height_trace") -> AFMImageStack: """ Unified interface to load AFM stacks from various file formats. High speed AFM videos can be saved as either individual frames within a folder or as multiple frames within a single file. This loader splits these two approaches and loads both into the common AFMImageStack object for processing. As well as the file formats exported from AFM instruments, this function also read raw and processed exports from playnano (NPZ, OME_TIF and HDF5). All data values with length units (i.e. m) are converted to nm. Parameters ---------- file_path : Path | str Path to the AFM data file or folder of files. channel : str Scan channel name. Returns ------- AFMImageStack Loaded image stack with metadata. """ logger.debug(f"Raw input path: {file_path}") file_path = Path(file_path).resolve() logger.debug(f"Resolved path: {file_path}") logger.debug(f"Loading AFM stack from {file_path} for channel '{channel}'") folder_loaders = { ".jpk": load_jpk_folder, ".spm": load_spm_folder, # Add others as needed } file_loaders = { ".h5-jpk": load_h5jpk, ".asd": load_asd_file, ".npz": load_npz_bundle, ".h5": load_h5_bundle, ".ome.tif": load_ome_tiff_stack, ".tif": load_ome_tiff_stack, # Add others as needed } # Load folder if file_path.is_dir(): logger.debug(f"Loading folder {file_path}") ext, loader = get_loader_for_folder(file_path, folder_loaders) logger.debug( f"Loading folder {file_path} with loader {loader.__name__} for extension {ext}" # noqa: E501 ) return loader(file_path, channel=channel) # Load file elif file_path.is_file(): logger.debug(f"Loading file {file_path}") ext, loader = get_loader_for_file(file_path, file_loaders, folder_loaders) logger.debug( f"Loading file {file_path} with loader {loader.__name__} for extension {ext}" # noqa: E501 ) return loader(file_path, channel=channel) else: raise FileNotFoundError(f"{file_path} is neither a file nor a directory.")