Source code for PyNutil.io.file_operations

import os
import json
from dataclasses import dataclass
from typing import Optional, Dict, Any

import numpy as np
import pandas as pd

from .meshview_writer import write_hemi_points_to_meshview
from .atlas_loader import resolve_atlas_labels


@dataclass
class SaveContext:
    """Groups the parameters needed by :func:`save_analysis_output`."""

    # Core data
    points: Optional[np.ndarray] = None
    objects: Optional[np.ndarray] = None
    label_df: Optional[pd.DataFrame] = None
    point_labels: Optional[np.ndarray] = None
    object_labels: Optional[np.ndarray] = None
    points_hemi_labels: Optional[np.ndarray] = None
    objects_hemi_labels: Optional[np.ndarray] = None
    atlas_labels: Optional[pd.DataFrame] = None
    point_values: Optional[np.ndarray] = None

    # Whether this is intensity mode (affects report filename)
    is_intensity: bool = False

    # Settings dict written to pynutil_settings.json (optional)
    settings_dict: Optional[Dict[str, Any]] = None

    # Output control
    prepend: str = ""
    colormap: str = "gray"


def _ensure_analysis_output_dirs(output_folder: str) -> None:
    os.makedirs(output_folder, exist_ok=True)
    for subdir in (
        "whole_series_report",
        "whole_series_meshview",
    ):
        os.makedirs(f"{output_folder}/{subdir}", exist_ok=True)


def save_analysis_output(ctx: SaveContext, output_folder: str):
    """
    Save the analysis output to the specified folder.

    Parameters
    ----------
    ctx : SaveContext
        All data and configuration needed for saving.
    output_folder : str
        The folder where the output will be saved.
    """
    # Create the output folder if it doesn't exist
    _ensure_analysis_output_dirs(output_folder)

    if ctx.label_df is not None:
        report_name = "intensity.csv" if ctx.is_intensity else "counts.csv"
        ctx.label_df.to_csv(
            f"{output_folder}/whole_series_report/{ctx.prepend}{report_name}",
            sep=";",
            na_rep="",
            index=False,
        )
    elif not ctx.prepend:
        print("No quantification found, so only coordinates will be saved.")
        print(
            "If you want to save the quantification, please run quantify_coordinates."
        )

    if ctx.points is not None:
        _save_whole_series_meshview(ctx, output_folder)

    _save_settings_json(ctx, output_folder)


def _save_settings_json(ctx: SaveContext, output_folder: str) -> None:
    """Write a reference settings JSON to *output_folder*."""
    if ctx.settings_dict is None:
        return
    settings_file_path = os.path.join(output_folder, "pynutil_settings.json")
    with open(settings_file_path, "w") as f:
        json.dump(ctx.settings_dict, f, indent=4)


def _save_whole_series_meshview(ctx: SaveContext, output_folder: str):
    """Write whole-series MeshView JSONs for pixels and centroids."""
    write_hemi_points_to_meshview(
        ctx.points,
        ctx.point_labels,
        ctx.points_hemi_labels,
        f"{output_folder}/whole_series_meshview/{ctx.prepend}pixels_meshview.json",
        ctx.atlas_labels,
        ctx.point_values,
        colormap=ctx.colormap,
    )
    if ctx.objects is not None:
        write_hemi_points_to_meshview(
            ctx.objects,
            ctx.object_labels,
            ctx.objects_hemi_labels,
            f"{output_folder}/whole_series_meshview/{ctx.prepend}objects_meshview.json",
            ctx.atlas_labels,
            colormap=ctx.colormap,
        )


[docs] def save_analysis( output_folder, result, atlas_labels, label_df=None, *, colormap="gray", settings_dict=None, ): """Save analysis output to the specified directory. Args: output_folder: Directory to write output files. result: ExtractionResult from coordinate extraction. atlas_labels: Atlas labels DataFrame (or AtlasData — ``.labels`` used). label_df: Whole-series quantification DataFrame. colormap: Colormap for MeshView intensity output. settings_dict: Optional dict written to pynutil_settings.json. """ atlas_labels = resolve_atlas_labels(atlas_labels) ctx = SaveContext( points=( result.points.points_in_internal_orientation(result.points.filtered_points()) if result else None ), objects=( result.objects.points_in_internal_orientation( result.objects.filtered_points() ) if (result and result.objects is not None) else None ), label_df=label_df, point_labels=result.points.filtered_labels() if result else None, object_labels=( result.objects.filtered_labels() if (result and result.objects is not None) else None ), points_hemi_labels=result.points.filtered_hemi_labels() if result else None, objects_hemi_labels=( result.objects.filtered_hemi_labels() if (result and result.objects is not None) else None ), atlas_labels=atlas_labels, point_values=result.points.filtered_point_values() if result else None, is_intensity=result.region_intensities is not None if result else False, settings_dict=settings_dict, colormap=colormap, ) save_analysis_output(ctx, output_folder) # Remap compressed IDs if present if label_df is not None and "original_idx" in label_df.columns: remapped = label_df.copy() remapped["idx"] = remapped["original_idx"] remapped = remapped.drop(columns=["original_idx"]) remapped.to_csv( f"{output_folder}/whole_series_report/counts.csv", sep=";", index=False, )