Skip to content

Miniscope Modules API

This section details the pipeline for processing calcium imaging data.

Miniscope Data Manager

ace_neuro.miniscope.miniscope_data_manager.MiniscopeDataManager

Bases: ExperimentDataManager, ABC

Manages raw Miniscope data import and storage.

Abstract Base Class for Miniscope data managers.

Attributes:

Name Type Description
line_num int

Experiment line number in experiments.csv.

time_stamps Optional[ndarray]

Array of frame timestamps in seconds.

frame_numbers Optional[ndarray]

List of frame indices.

all_movie_filepaths List[Union[Path, str]]

All discovered .avi movie files.

chosen_movie_filepaths Optional[List[Union[str, Path]]]

Subset selected by filenames parameter.

movie movie

Loaded CaImAn movie object.

fr float

Frame rate from metadata.

Source code in src/ace_neuro/miniscope/miniscope_data_manager.py
class MiniscopeDataManager(ExperimentDataManager, ABC):
    """Manages raw Miniscope data import and storage.

    Abstract Base Class for Miniscope data managers.

    Attributes:
        line_num: Experiment line number in experiments.csv.
        time_stamps: Array of frame timestamps in seconds.
        frame_numbers: List of frame indices.
        all_movie_filepaths: All discovered .avi movie files.
        chosen_movie_filepaths: Subset selected by filenames parameter.
        movie: Loaded CaImAn movie object.
        fr: Frame rate from metadata.
    """

    _registry: List[Type['MiniscopeDataManager']] = []
    line_num: int
    time_stamps: Optional[np.ndarray]
    frame_numbers: Optional[np.ndarray]
    all_movie_filepaths: List[Union[Path, str]]
    chosen_movie_filepaths: Optional[List[Union[str, Path]]]
    movie: movie
    fr: float
    projections: Any
    preprocessed_movie_filepath: Optional[str]
    coords: Optional[Dict[str, int]]
    motion_corrected_movie_filepath: Optional[str]
    CNMFE_obj: Any
    estimates_filepath: Optional[str]
    opts_caiman_filepath: Optional[str]
    analysis_params: Optional[Dict[str, Any]]
    dview: Any
    opts_caiman: Any
    ca_events_idx: Any
    PSD_spect: Any
    t_spect: Any
    freqs_spect: Any
    p_spect: Any
    miniscope_phases: Any
    filter_object: Any
    miniscope_events: Any
    Cn: Optional[np.ndarray]
    filenames: Optional[List[str]]

    def __init_subclass__(cls, **kwargs: Any) -> None:
        super().__init_subclass__(**kwargs)
        if cls not in cls._registry:
            cls._registry.append(cls)

    @classmethod
    def create(
        cls: Type[T], 
        line_num: int, 
        project_path: Optional[Union[str, Path]] = None,
        data_path: Optional[Union[str, Path]] = None,
        **kwargs: Any
    ) -> T:
        """Factory method to select the correct subclass for the directory.

        Args:
            line_num: Experiment line number.
            project_path: Optional explicit project repository path.
            data_path: Optional explicit data storage path.
            **kwargs: Additional arguments for subclass initialization.
        """
        temp_edm = ExperimentDataManager(
            line_num, 
            project_path=project_path, 
            data_path=data_path,
            auto_import_metadata=True, 
            auto_import_analysis_params=False
        )
        directory = temp_edm.get_miniscope_directory()

        if directory is None:
             raise ValueError(f"No miniscope directory set in metadata for line {line_num}")

        for subclass in cls._registry:
            if subclass.can_handle(directory):
                return cast(T, subclass(
                    line_num=line_num, 
                    project_path=project_path,
                    data_path=data_path,
                    **kwargs
                ))

        raise ValueError(f"No MiniscopeDataManager subclass found that can handle directory: {directory}")

    @classmethod
    @abstractmethod
    def can_handle(cls, directory: Union[str, Path]) -> bool:
        """Return True if this class can handle the format in the given directory."""
        pass

    def __init__(
        self, 
        line_num: int, 
        project_path: Optional[Union[str, Path]] = None,
        data_path: Optional[Union[str, Path]] = None,
        filenames: List[str] = [], 
        auto_import_data: bool = True
    ) -> None:
        """Initialize data manager and optionally load movie data.

        Args:
            line_num: Row number in experiments.csv to load.
            project_path: Optional explicit project repository path.
            data_path: Optional explicit data storage path.
            filenames: Optional list of specific movie filenames to load.
            auto_import_data: If True, automatically load movie and metadata.
        """
        super().__init__(line_num, project_path=project_path, data_path=data_path)
        self.line_num = line_num
        self.time_stamps = None
        self.frame_numbers = None

        experiments_csv = self.project_path / "experiments.csv"
        file_downloader.verify_file_by_line(
            line_num, 
            experiments_csv, 
            "miniscope", 
            filenames,
            base_file_path=self.data_path
        )
        self.all_movie_filepaths = cast(List[Union[Path, str]], self._find_movie_file_paths())
        self.chosen_movie_filepaths = self._get_specific_filepaths(filenames)

        if (auto_import_data):
            self.load_attributes(self.chosen_movie_filepaths if self.chosen_movie_filepaths else self.all_movie_filepaths)

        #Attributes below are filled in automatically during the miniscope_pipeline pipeline: preprocessing->processing->postprocessing

        self.projections = None
        self.preprocessed_movie_filepath = None #Your preprocessed movie must be saved to disk and its filepath stored here before processing
        self.coords = None #contains the coordinates/shape of your cropped movie
        self.motion_corrected_movie_filepath = None
        self.CNMFE_obj = None
        self.estimates_filepath = None
        self.dview = None
        self.opts_caiman = None
        self.ca_events_idx = None
        self.PSD_spect = None
        self.t_spect = None
        self.freqs_spect = None
        self.p_spect = None
        self.miniscope_phases = None
        self.filter_object = None


    def load_attributes(self, filepaths: List[Union[str, Path]]) -> None:
        """Load movie data and metadata from disk.

        Populates metadata, timestamps, movie array, events, and frame rate.

        Args:
            filepaths: List of movie file paths to load.
        """
        if self.metadata is None:
            self.metadata = {}
        self.metadata.update(self._get_miniscope_metadata()) # add miniscope metadata to overall metadata
        ts, fn = self._get_timestamps()
        self.time_stamps = np.array(ts) if ts is not None else None
        self.frame_numbers = np.array(fn) if fn is not None else None
        self.movie = self._get_movies(filepaths)  # import calcium imaging data
        self.miniscope_events = self._get_miniscope_events()
        self.fr = float(self.metadata['frameRate']) if self.metadata else 30.0

    @abstractmethod
    def sync_timestamps(
        self, 
        ephys_dm: Optional[Any] = None, 
        channel_name: Optional[str] = None, 
        **kwargs: Any
    ) -> Tuple[np.ndarray, np.ndarray]:
        """
        Synchronize miniscope frame timestamps to ephys time.

        Args:
            ephys_dm: Optional EphysDataManager to sync against.
            channel_name: Optional channel name for the ephys_dm.

        Returns:
            Tuple containing:
            - tCaIm (np.ndarray): The aligned miniscope frame timestamps in ephys time.
            - low_confidence_periods (np.ndarray): Array of [start, end] indices where gap interpolation occurred.
        """
        pass

    def convert_ca_movies(
        self, 
        filenames: Optional[List[str]] = None, 
        new_file_type: str = '.tif', 
        join_movies: bool = False, 
        metadata_convert: bool = True
    ) -> None:
        """
        Convert calcium movies from one type to another. File types must be supported by CaImAn.

        The new filename(s) is based on the first filename in 'filenames', with new_file_type appended.
        'join_movies' determines whether all movie files in 'filenames' are combined into a single movie,
        or whether each file is converted separately.

        If 'filenames' is None, the method will attempt to load filenames from self.movieFilePaths.
        """
        print("Converting movies...")
        original_filenames = filenames  # Preserve the original argument
        error_videos = []

        # If no filenames provided, try to load from self.movieFilePaths
        if filenames is None:
            filenames_list: List[str] = [str(p) for p in self.all_movie_filepaths]
        elif not isinstance(filenames, list):
            filenames_list = [filenames]
        else:
            filenames_list = filenames

        # Use the first filename as a basis for the new filename.
        base_new_filename = os.path.splitext(filenames_list[0])[0]

        # If self.movie exists and no filenames were explicitly provided, use the existing movie.
        if hasattr(self, 'movie') and original_filenames is None:
            new_filename = f"{base_new_filename}{new_file_type}"
            self.movie.save(new_filename, compress=0)
        else:
            if join_movies:
                # Join movies: load the movie chain and save as a single new movie.
                try:
                    movies = cm.load_movie_chain(filenames)
                    new_filename = f"{base_new_filename}{new_file_type}"
                    movies.save(new_filename)
                except Exception as e:
                    print(f"Error converting joined movies: {e}")
                    error_videos.extend(filenames_list)
            else:
                # Convert each movie separately.
                for filename in filenames_list:
                    try:
                        # If the file doesn't exist, assume it might be in a default Miniscope directory.
                        if not os.path.isfile(filename):
                            default_dir = os.path.join(self.metadata.get('calcium imaging directory', ''), 'Miniscope') if self.metadata else ''
                            filename = os.path.join(default_dir, str(filename))
                        movie = cm.load(filename)
                        new_filename = f"{os.path.splitext(filename)[0]}{new_file_type}"
                        movie.save(new_filename, compress=0)
                    except (OSError, ValueError, MemoryError) as e:
                        print(f"Error converting movie '{filename}': {e}")
                        error_videos.append(filename)

        if metadata_convert:
            self._meta_data_converter()

        if error_videos:
            print(f"ERRORS with: {error_videos}")
            print("Consider investigating")


    def _meta_data_converter(self) -> None:
        """Convert and merge metadata from multiple JSON files.

        Creates a unified metaDataTif.json file combining animal ID, frame rate,
        date, and original metadata from multiple source files.
        """
        fileExts = self._find_metadata_paths()
        for fileExt in fileExts:
            with open(fileExt) as f:
                data = json.loads(f.read())
                if 'animalID' in data:
                    ext = fileExt.replace('\\metaData.json', '\\Miniscope\\metaData.json')
                    animalID = data['animalID']
                    timeStamp = data['recordingStartTime']
                    year = str(timeStamp['year'])
                    month = str('%02d' % timeStamp['month'])
                    day = str('%02d' % timeStamp['day'])
                    second = str('%02d' % timeStamp['second'])
                    minute = str('%02d' % timeStamp['minute'])
                    hour = str('%02d' % timeStamp['hr'])
                    date = year + month + day + '_' + hour + minute + second
                    with open(ext) as d:
                        data2 = json.loads(d.read())
                        if 'frameRate' in data2:
                            try:
                                frameRate = float(data2['frameRate'])
                            except (ValueError, KeyError):
                                frameRate = 30.0
                        else:
                            frameRate = 30.0
                        jdict = {'origin': animalID, 'fps': frameRate, 'date': date,
                                    'orig_meta': [data, data2]}
                        jsonFile = json.dumps(jdict, indent=4)
                        newFileName = ext.replace('\\metaData.json', '\\metaDataTif.json')
                        n = open(newFileName, 'w')
                        n.write(jsonFile)
                        n.close()


    @abstractmethod
    def _get_miniscope_metadata(self) -> Dict[str, Any]:
        pass

    @abstractmethod
    def _get_timestamps(self) -> Tuple[Any, Any]:
        pass

    def _get_movies(self, filenames: Optional[Union[str, Path, List[Union[str, Path]]]] = None) -> movie:
        """Import calcium imaging data. Not necessary if using processCaMovies().
        FILENAMES can be a single movie file or a list of movie files (in the order that you want them). 
        If FILENAMES doesn't point to a file (either absolute or relative path from the PWD), 
        it will append the path to the calcium imaging directory to the front of the filename."""

        print(f"Converting these filepaths into caiman movies: {filenames}")
        if filenames is None:
            filenames = self.all_movie_filepaths

        # Convert PosixPath objects to strings if necessary.
        if isinstance(filenames, list):
            f_names: List[str] = [str(f) for f in filenames]
            print(f_names)
            m: movie = cm.load_movie_chain(f_names)
        else:
            f_name: str = str(filenames)
            m = cm.load(f_name)

        return m

    def _get_specific_filepaths(self, filenames: List[str]) -> Optional[List[Union[str, Path]]]:
        """Filter movie paths to only those matching provided filenames.

        Args:
            filenames: List of basenames to match (e.g., ['0.avi', '1.avi']).

        Returns:
            List of full paths matching the specified filenames, or None.
        """
        if filenames is None or not isinstance(filenames, list) or len(filenames) == 0:
            return None

        matched_paths: List[Union[str, Path]] = []

        for path in self.all_movie_filepaths:
            basename = os.path.basename(str(path))  # Extract basename (e.g., '0.avi' from '/path/to/0.avi')
            if basename in filenames:
                matched_paths.append(path)
        return matched_paths


    @abstractmethod
    def _get_miniscope_events(self) -> Any:
        pass

    @property
    def _calcium_imaging_directory(self) -> str:
        """Returns the directory where calcium imaging data is stored."""
        return str(self.metadata['calcium imaging directory']) if self.metadata else ""

    def _find_file_paths(self, suffix: str, prefix: str = "") -> Union[str, List[str]]:
        """Generalized helper to find files with the given suffix and prefix."""
        filepaths = PathFinder.find(directory=self._calcium_imaging_directory, suffix=suffix, prefix=prefix)

        #handle the case where filepaths is a list with only one item
        if isinstance(filepaths, list) and len(filepaths) == 1:
            return str(filepaths[0])
        elif isinstance(filepaths, list):
            return [str(f) for f in filepaths]
        return str(filepaths)

    def _find_metadata_paths(self) -> List[str]:
        """Finds and returns the metadata JSON file path."""
        res = self._find_file_paths(suffix=".json", prefix="metaData")
        return [res] if isinstance(res, str) else res

    def _find_timestamps_path(self) -> str:
        """Finds and returns the timestamps CSV file path."""
        res = self._find_file_paths(suffix=".csv", prefix="timeStamps")
        return res[0] if isinstance(res, list) and res else str(res)

    def _find_movie_file_paths(self) -> List[str]:
        """Finds and returns the list of movie file paths (.avi)."""
        res = self._find_file_paths(suffix=".avi")
        return [res] if isinstance(res, str) else res

    def _extract_numeric_suffix(self, filename: str) -> str:
        """
        Extracts and returns the substring of filename starting from the first digit.
        If no digit is found, returns the original filename.
        """
        for i, char in enumerate(filename):
            if char.isdigit():
                return filename[i:]
        return filename

__init__(line_num, project_path=None, data_path=None, filenames=[], auto_import_data=True)

Initialize data manager and optionally load movie data.

Parameters:

Name Type Description Default
line_num int

Row number in experiments.csv to load.

required
project_path Optional[Union[str, Path]]

Optional explicit project repository path.

None
data_path Optional[Union[str, Path]]

Optional explicit data storage path.

None
filenames List[str]

Optional list of specific movie filenames to load.

[]
auto_import_data bool

If True, automatically load movie and metadata.

True
Source code in src/ace_neuro/miniscope/miniscope_data_manager.py
def __init__(
    self, 
    line_num: int, 
    project_path: Optional[Union[str, Path]] = None,
    data_path: Optional[Union[str, Path]] = None,
    filenames: List[str] = [], 
    auto_import_data: bool = True
) -> None:
    """Initialize data manager and optionally load movie data.

    Args:
        line_num: Row number in experiments.csv to load.
        project_path: Optional explicit project repository path.
        data_path: Optional explicit data storage path.
        filenames: Optional list of specific movie filenames to load.
        auto_import_data: If True, automatically load movie and metadata.
    """
    super().__init__(line_num, project_path=project_path, data_path=data_path)
    self.line_num = line_num
    self.time_stamps = None
    self.frame_numbers = None

    experiments_csv = self.project_path / "experiments.csv"
    file_downloader.verify_file_by_line(
        line_num, 
        experiments_csv, 
        "miniscope", 
        filenames,
        base_file_path=self.data_path
    )
    self.all_movie_filepaths = cast(List[Union[Path, str]], self._find_movie_file_paths())
    self.chosen_movie_filepaths = self._get_specific_filepaths(filenames)

    if (auto_import_data):
        self.load_attributes(self.chosen_movie_filepaths if self.chosen_movie_filepaths else self.all_movie_filepaths)

    #Attributes below are filled in automatically during the miniscope_pipeline pipeline: preprocessing->processing->postprocessing

    self.projections = None
    self.preprocessed_movie_filepath = None #Your preprocessed movie must be saved to disk and its filepath stored here before processing
    self.coords = None #contains the coordinates/shape of your cropped movie
    self.motion_corrected_movie_filepath = None
    self.CNMFE_obj = None
    self.estimates_filepath = None
    self.dview = None
    self.opts_caiman = None
    self.ca_events_idx = None
    self.PSD_spect = None
    self.t_spect = None
    self.freqs_spect = None
    self.p_spect = None
    self.miniscope_phases = None
    self.filter_object = None

can_handle(directory) abstractmethod classmethod

Return True if this class can handle the format in the given directory.

Source code in src/ace_neuro/miniscope/miniscope_data_manager.py
@classmethod
@abstractmethod
def can_handle(cls, directory: Union[str, Path]) -> bool:
    """Return True if this class can handle the format in the given directory."""
    pass

convert_ca_movies(filenames=None, new_file_type='.tif', join_movies=False, metadata_convert=True)

Convert calcium movies from one type to another. File types must be supported by CaImAn.

The new filename(s) is based on the first filename in 'filenames', with new_file_type appended. 'join_movies' determines whether all movie files in 'filenames' are combined into a single movie, or whether each file is converted separately.

If 'filenames' is None, the method will attempt to load filenames from self.movieFilePaths.

Source code in src/ace_neuro/miniscope/miniscope_data_manager.py
def convert_ca_movies(
    self, 
    filenames: Optional[List[str]] = None, 
    new_file_type: str = '.tif', 
    join_movies: bool = False, 
    metadata_convert: bool = True
) -> None:
    """
    Convert calcium movies from one type to another. File types must be supported by CaImAn.

    The new filename(s) is based on the first filename in 'filenames', with new_file_type appended.
    'join_movies' determines whether all movie files in 'filenames' are combined into a single movie,
    or whether each file is converted separately.

    If 'filenames' is None, the method will attempt to load filenames from self.movieFilePaths.
    """
    print("Converting movies...")
    original_filenames = filenames  # Preserve the original argument
    error_videos = []

    # If no filenames provided, try to load from self.movieFilePaths
    if filenames is None:
        filenames_list: List[str] = [str(p) for p in self.all_movie_filepaths]
    elif not isinstance(filenames, list):
        filenames_list = [filenames]
    else:
        filenames_list = filenames

    # Use the first filename as a basis for the new filename.
    base_new_filename = os.path.splitext(filenames_list[0])[0]

    # If self.movie exists and no filenames were explicitly provided, use the existing movie.
    if hasattr(self, 'movie') and original_filenames is None:
        new_filename = f"{base_new_filename}{new_file_type}"
        self.movie.save(new_filename, compress=0)
    else:
        if join_movies:
            # Join movies: load the movie chain and save as a single new movie.
            try:
                movies = cm.load_movie_chain(filenames)
                new_filename = f"{base_new_filename}{new_file_type}"
                movies.save(new_filename)
            except Exception as e:
                print(f"Error converting joined movies: {e}")
                error_videos.extend(filenames_list)
        else:
            # Convert each movie separately.
            for filename in filenames_list:
                try:
                    # If the file doesn't exist, assume it might be in a default Miniscope directory.
                    if not os.path.isfile(filename):
                        default_dir = os.path.join(self.metadata.get('calcium imaging directory', ''), 'Miniscope') if self.metadata else ''
                        filename = os.path.join(default_dir, str(filename))
                    movie = cm.load(filename)
                    new_filename = f"{os.path.splitext(filename)[0]}{new_file_type}"
                    movie.save(new_filename, compress=0)
                except (OSError, ValueError, MemoryError) as e:
                    print(f"Error converting movie '{filename}': {e}")
                    error_videos.append(filename)

    if metadata_convert:
        self._meta_data_converter()

    if error_videos:
        print(f"ERRORS with: {error_videos}")
        print("Consider investigating")

create(line_num, project_path=None, data_path=None, **kwargs) classmethod

Factory method to select the correct subclass for the directory.

Parameters:

Name Type Description Default
line_num int

Experiment line number.

required
project_path Optional[Union[str, Path]]

Optional explicit project repository path.

None
data_path Optional[Union[str, Path]]

Optional explicit data storage path.

None
**kwargs Any

Additional arguments for subclass initialization.

{}
Source code in src/ace_neuro/miniscope/miniscope_data_manager.py
@classmethod
def create(
    cls: Type[T], 
    line_num: int, 
    project_path: Optional[Union[str, Path]] = None,
    data_path: Optional[Union[str, Path]] = None,
    **kwargs: Any
) -> T:
    """Factory method to select the correct subclass for the directory.

    Args:
        line_num: Experiment line number.
        project_path: Optional explicit project repository path.
        data_path: Optional explicit data storage path.
        **kwargs: Additional arguments for subclass initialization.
    """
    temp_edm = ExperimentDataManager(
        line_num, 
        project_path=project_path, 
        data_path=data_path,
        auto_import_metadata=True, 
        auto_import_analysis_params=False
    )
    directory = temp_edm.get_miniscope_directory()

    if directory is None:
         raise ValueError(f"No miniscope directory set in metadata for line {line_num}")

    for subclass in cls._registry:
        if subclass.can_handle(directory):
            return cast(T, subclass(
                line_num=line_num, 
                project_path=project_path,
                data_path=data_path,
                **kwargs
            ))

    raise ValueError(f"No MiniscopeDataManager subclass found that can handle directory: {directory}")

load_attributes(filepaths)

Load movie data and metadata from disk.

Populates metadata, timestamps, movie array, events, and frame rate.

Parameters:

Name Type Description Default
filepaths List[Union[str, Path]]

List of movie file paths to load.

required
Source code in src/ace_neuro/miniscope/miniscope_data_manager.py
def load_attributes(self, filepaths: List[Union[str, Path]]) -> None:
    """Load movie data and metadata from disk.

    Populates metadata, timestamps, movie array, events, and frame rate.

    Args:
        filepaths: List of movie file paths to load.
    """
    if self.metadata is None:
        self.metadata = {}
    self.metadata.update(self._get_miniscope_metadata()) # add miniscope metadata to overall metadata
    ts, fn = self._get_timestamps()
    self.time_stamps = np.array(ts) if ts is not None else None
    self.frame_numbers = np.array(fn) if fn is not None else None
    self.movie = self._get_movies(filepaths)  # import calcium imaging data
    self.miniscope_events = self._get_miniscope_events()
    self.fr = float(self.metadata['frameRate']) if self.metadata else 30.0

sync_timestamps(ephys_dm=None, channel_name=None, **kwargs) abstractmethod

Synchronize miniscope frame timestamps to ephys time.

Parameters:

Name Type Description Default
ephys_dm Optional[Any]

Optional EphysDataManager to sync against.

None
channel_name Optional[str]

Optional channel name for the ephys_dm.

None

Returns:

Type Description
ndarray

Tuple containing:

ndarray
  • tCaIm (np.ndarray): The aligned miniscope frame timestamps in ephys time.
Tuple[ndarray, ndarray]
  • low_confidence_periods (np.ndarray): Array of [start, end] indices where gap interpolation occurred.
Source code in src/ace_neuro/miniscope/miniscope_data_manager.py
@abstractmethod
def sync_timestamps(
    self, 
    ephys_dm: Optional[Any] = None, 
    channel_name: Optional[str] = None, 
    **kwargs: Any
) -> Tuple[np.ndarray, np.ndarray]:
    """
    Synchronize miniscope frame timestamps to ephys time.

    Args:
        ephys_dm: Optional EphysDataManager to sync against.
        channel_name: Optional channel name for the ephys_dm.

    Returns:
        Tuple containing:
        - tCaIm (np.ndarray): The aligned miniscope frame timestamps in ephys time.
        - low_confidence_periods (np.ndarray): Array of [start, end] indices where gap interpolation occurred.
    """
    pass

Preprocessor

ace_neuro.miniscope.miniscope_preprocessor.MiniscopePreprocessor

Preprocessor for calcium imaging movies before CNMF-E analysis.

Handles cropping, detrending, and DF/F computation to prepare raw miniscope recordings for source extraction.

Attributes:

Name Type Description
data_manager MiniscopeDataManager

MiniscopeDataManager with loaded movie.

frame_rate float

Movie frame rate in Hz.

Source code in src/ace_neuro/miniscope/miniscope_preprocessor.py
class MiniscopePreprocessor:
    """Preprocessor for calcium imaging movies before CNMF-E analysis.

    Handles cropping, detrending, and DF/F computation to prepare
    raw miniscope recordings for source extraction.

    Attributes:
        data_manager: MiniscopeDataManager with loaded movie.
        frame_rate: Movie frame rate in Hz.
    """

    data_manager: 'MiniscopeDataManager'
    frame_rate: float

    def __init__(self, data_manager: 'MiniscopeDataManager') -> None:
        """Initialize preprocessor with data manager.

        Args:
            data_manager: MiniscopeDataManager with movie attribute.
        """
        self.data_manager = data_manager
        self.frame_rate = float(data_manager.movie.fr)


    def preprocess_calcium_movie(
        self, 
        coords_dict: Optional[Dict[str, int]] = None, 
        crop: bool = False, 
        detrend_method: Optional[str] = None, 
        df_over_f: bool = False, 
        crop_job_name_for_file: str = "_cropped",
        secs_window: float = 5, 
        quantile_min: float = 8, 
        df_over_f_method: str = 'delta_f_over_sqrt_f', 
        headless: bool = False
    ) -> 'MiniscopeDataManager':
        """Run preprocessing steps based on provided flags.
           coords_dict: is passed in and represents what you want the final coordinates for the movie to be in the form {'x0': A, 'y0': B, 'x1': C, 'y1': D}"""

        steps_applied = ['preprocessed']

        if crop:
            self.data_manager.projections = self.compute_projections(self.data_manager.movie)
            movie_height = self.data_manager.movie.shape[1]
            movie_width = self.data_manager.movie.shape[2]
            final_coords = self.get_crop_coordinates(coords_dict, self.data_manager.projections, movie_height, movie_width, headless=headless)
            if final_coords is not None:
                self.data_manager.movie, cropped_movie_filepath = self.crop_movie(self.data_manager.movie, final_coords)
                self.data_manager.preprocessed_movie_filepath = cropped_movie_filepath
                self.data_manager.coords = final_coords # Store the coordinates used

        if detrend_method:
            self.data_manager.movie = self.detrend_movie(self.data_manager.movie, method=detrend_method, plot_trend=not headless)
            steps_applied.append('_detrended')

        if df_over_f:
            self.data_manager.movie = self.compute_df_over_f(self.data_manager.movie, secs_window=secs_window, quantile_min=quantile_min, method=df_over_f_method)
            steps_applied.append('_dFoverF')

        movie_file_name = ''.join(steps_applied)

        self.data_manager.preprocessed_movie_filepath = MovieIO.save_movie(self.data_manager, movie_file_name)

        print(f"This is the movie shape after preprocessing: {self.data_manager.movie.shape}")

        return self.data_manager





    def compute_projections(self, movie: Optional[cm.movie] = None) -> Projections:
        """Compute spatial and temporal projections of the movie.

        Calculates max, min, mean, median, std, range projections and
        mean fluorescence time series.

        Args:
            movie: CaImAn movie object to compute projections from.

        Returns:
            Projections object containing all computed projections.
        """
        print("\n\nComputing projections...\n")

        operations = {
            'max': lambda m: np.amax(m, axis=0),
            'std': lambda m: np.std(m, axis=0),
            'min': lambda m: np.amin(m, axis=0),
            'mean': lambda m: np.mean(m, axis=0),
            'median': lambda m: np.median(m, axis=0),
            'time': lambda m: m.mean(axis=(1,2)),
        }

        results = {}
        for name, op in tqdm(operations.items(), desc='Computing Projections'):
            results[name] = op(movie)

        results['range'] = results['max'] - results['min']

        return Projections(
            results['max'],
            results['std'],
            results['min'],
            results['mean'],
            results['median'],
            results['range'],
            results['time']
        )    


    def get_crop_coordinates(
        self, 
        coords_dict: Optional[Dict[str, int]], 
        projections: Projections, 
        movie_height: int, 
        movie_width: int, 
        headless: bool = False
    ) -> Optional[Dict[str, int]]:
        """Get crop coordinates from GUI or provided coordinates.

        In headless mode, returns the provided coordinates directly without
        opening a GUI. If no coordinates are available in headless mode,
        returns None with a warning.

        In interactive mode, opens crop GUI (pre-populated with coords_dict
        if available) for visual adjustment.

        Args:
            coords_dict: Dict with x0, y0, x1, y1 keys, or None for GUI.
            projections: Projections object for GUI visualization.
            movie_height: Height of the movie in pixels.
            movie_width: Width of the movie in pixels.
            headless: If True, bypass GUI and use coords_dict directly.

        Returns:
            Dict with x0, y0, x1, y1 keys, or None if no coordinates available.
        """
        if headless:
            if coords_dict is None:
                print("WARNING: crop=True but no crop coordinates found in analysis_parameters.csv. "
                      "Skipping crop in headless mode. Provide 'crop' coordinates "
                      "in your analysis_parameters.csv to crop in headless mode.", flush=True)
                return None
            print(f"HEADLESS: Cropping with coordinates from analysis_parameters.csv: {coords_dict}", flush=True)
            return coords_dict
        else:
            return crop_gui(coords_dict, projections, movie_height, movie_width)


    def crop_movie(self, movie: cm.movie, coords_dict: Dict[str, int]) -> Tuple[cm.movie, str]:
        """Crop a movie using the given coordinates.

        Performs y-coordinate flipping (GUI origin is bottom-left, numpy
        origin is top-left), sorts coordinates, and slices the movie array.

        Args:
            movie: CaImAn movie to crop.
            coords_dict: Dict with x0, y0, x1, y1 keys (in GUI coordinate space).

        Returns:
            Tuple of (cropped_movie, coords_string).
        """
        # Flip y-coordinates (GUI origin is bottom-left; numpy origin is top-left)
        y0_flipped = movie.shape[1] - coords_dict['y1']
        y1_flipped = movie.shape[1] - coords_dict['y0']

        # Sort coordinates
        y0, y1 = sorted([y0_flipped, y1_flipped])
        x0, x1 = sorted([coords_dict['x0'], coords_dict['x1']])

        #crop movie using our numpy coordinates
        cropped_movie = movie[:, y0:y1, x0:x1]

        #Keep coords_dict in GUI notation so that it will display properly in the GUI if you want to view them again
        coords_string = f'({coords_dict["x0"]},{coords_dict["y0"]}, {coords_dict["x1"]},{coords_dict["y1"]})'

        return cropped_movie, coords_string


    def detrend_movie(
        self, 
        movie: cm.movie, 
        method: str = 'median', 
        plot_trend: bool = True
    ) -> cm.movie:
        """Remove slow temporal trends from the movie.

        Supports linear detrending or median-based debleaching to correct
        for photobleaching and other drift.

        Args:
            movie: CaImAn movie to detrend.
            method: 'linear' for scipy detrend, 'median' for CaImAn debleach.
            plot_trend: If True, display before/after comparison plot.

        Returns:
            Detrended CaImAn movie.
        """
        detrended_movie = movie # Initialize with the original movie
        try:                    
            if method == 'linear':
                detrended_movie = detrend(movie, axis=0)
            elif method == 'median':
                # Manual median-based detrending (debleach was removed in newer CaIman versions)
                # Subtract the running median baseline to correct for photobleaching
                mean_trace = np.mean(movie, axis=(1, 2))
                median_baseline = np.median(mean_trace)
                trend = mean_trace - median_baseline
                # Subtract trend from each frame
                detrended_movie = movie - trend[:, np.newaxis, np.newaxis]
        except (ValueError, np.linalg.LinAlgError) as e:
            print(f"Detrending failed ({e}), returning original movie")
            return movie

        if plot_trend and detrended_movie is not None:
            fig, ax = plt.subplots()
            ax.set_xlabel('Frames')
            ax.set_ylabel('Mean Fluorescence')

            # Plot original data
            original_mean = np.mean(movie, axis=(1, 2))
            ax.plot(original_mean, label='Original Data', color='blue')

            # Plot detrended data
            detrended_mean = np.mean(detrended_movie, axis=(1, 2))
            ax.plot(detrended_mean, label='Detrended Data', color='red', linestyle='--')

            ax.legend()
            ax.grid(True)
            plt.tight_layout()
            plt.show()

        if isinstance(detrended_movie, np.ndarray):
            print("Ensuring that the movie that is returned is a caiman movie, not a numpy array...")
            detrended_movie = cm.movie(detrended_movie, fr=self.frame_rate)

        print('Detrending was successful')
        return detrended_movie


    def compute_df_over_f(
        self, 
        movie: Union[cm.movie, np.ndarray], 
        secs_window: float = 5, 
        quantile_min: float = 8, 
        method: str = 'delta_f_over_sqrt_f'
    ) -> cm.movie:
        """Compute DF/F or DF/sqrt(F) normalization of the movie.

        Normalizes fluorescence to percentage changes relative to baseline,
        which is estimated using a sliding window and quantile.

        Args:
            movie: CaImAn movie to normalize.
            secs_window: Window size in seconds for baseline estimation.
            quantile_min: Percentile for baseline (0-100).
            method: 'delta_f_over_sqrt_f' or 'delta_f_over_f'.

        Returns:
            Normalized CaImAn movie.
        """
        try:
            if np.min(movie) < 0:
                min_val = np.min(movie)
                movie = movie - min_val
            if np.min(movie) == 0:
                movie = movie + 1

            if isinstance(movie, np.ndarray):
                print("Ensuring that movie is turned back into a caiman object, not a numpy array...")
                movie = cm.movie(movie, fr=self.frame_rate)

            processed_movie, _ = cm_movie.computeDFF(movie, secs_window, quantile_min, method)
            print("Computing df over f / sqrt f was successful")
            return processed_movie

        except (ZeroDivisionError, FloatingPointError, ValueError) as e:
            raise ProcessingError(f"Computing df over f failed: {e}") from e

__init__(data_manager)

Initialize preprocessor with data manager.

Parameters:

Name Type Description Default
data_manager MiniscopeDataManager

MiniscopeDataManager with movie attribute.

required
Source code in src/ace_neuro/miniscope/miniscope_preprocessor.py
def __init__(self, data_manager: 'MiniscopeDataManager') -> None:
    """Initialize preprocessor with data manager.

    Args:
        data_manager: MiniscopeDataManager with movie attribute.
    """
    self.data_manager = data_manager
    self.frame_rate = float(data_manager.movie.fr)

compute_df_over_f(movie, secs_window=5, quantile_min=8, method='delta_f_over_sqrt_f')

Compute DF/F or DF/sqrt(F) normalization of the movie.

Normalizes fluorescence to percentage changes relative to baseline, which is estimated using a sliding window and quantile.

Parameters:

Name Type Description Default
movie Union[movie, ndarray]

CaImAn movie to normalize.

required
secs_window float

Window size in seconds for baseline estimation.

5
quantile_min float

Percentile for baseline (0-100).

8
method str

'delta_f_over_sqrt_f' or 'delta_f_over_f'.

'delta_f_over_sqrt_f'

Returns:

Type Description
movie

Normalized CaImAn movie.

Source code in src/ace_neuro/miniscope/miniscope_preprocessor.py
def compute_df_over_f(
    self, 
    movie: Union[cm.movie, np.ndarray], 
    secs_window: float = 5, 
    quantile_min: float = 8, 
    method: str = 'delta_f_over_sqrt_f'
) -> cm.movie:
    """Compute DF/F or DF/sqrt(F) normalization of the movie.

    Normalizes fluorescence to percentage changes relative to baseline,
    which is estimated using a sliding window and quantile.

    Args:
        movie: CaImAn movie to normalize.
        secs_window: Window size in seconds for baseline estimation.
        quantile_min: Percentile for baseline (0-100).
        method: 'delta_f_over_sqrt_f' or 'delta_f_over_f'.

    Returns:
        Normalized CaImAn movie.
    """
    try:
        if np.min(movie) < 0:
            min_val = np.min(movie)
            movie = movie - min_val
        if np.min(movie) == 0:
            movie = movie + 1

        if isinstance(movie, np.ndarray):
            print("Ensuring that movie is turned back into a caiman object, not a numpy array...")
            movie = cm.movie(movie, fr=self.frame_rate)

        processed_movie, _ = cm_movie.computeDFF(movie, secs_window, quantile_min, method)
        print("Computing df over f / sqrt f was successful")
        return processed_movie

    except (ZeroDivisionError, FloatingPointError, ValueError) as e:
        raise ProcessingError(f"Computing df over f failed: {e}") from e

compute_projections(movie=None)

Compute spatial and temporal projections of the movie.

Calculates max, min, mean, median, std, range projections and mean fluorescence time series.

Parameters:

Name Type Description Default
movie Optional[movie]

CaImAn movie object to compute projections from.

None

Returns:

Type Description
Projections

Projections object containing all computed projections.

Source code in src/ace_neuro/miniscope/miniscope_preprocessor.py
def compute_projections(self, movie: Optional[cm.movie] = None) -> Projections:
    """Compute spatial and temporal projections of the movie.

    Calculates max, min, mean, median, std, range projections and
    mean fluorescence time series.

    Args:
        movie: CaImAn movie object to compute projections from.

    Returns:
        Projections object containing all computed projections.
    """
    print("\n\nComputing projections...\n")

    operations = {
        'max': lambda m: np.amax(m, axis=0),
        'std': lambda m: np.std(m, axis=0),
        'min': lambda m: np.amin(m, axis=0),
        'mean': lambda m: np.mean(m, axis=0),
        'median': lambda m: np.median(m, axis=0),
        'time': lambda m: m.mean(axis=(1,2)),
    }

    results = {}
    for name, op in tqdm(operations.items(), desc='Computing Projections'):
        results[name] = op(movie)

    results['range'] = results['max'] - results['min']

    return Projections(
        results['max'],
        results['std'],
        results['min'],
        results['mean'],
        results['median'],
        results['range'],
        results['time']
    )    

crop_movie(movie, coords_dict)

Crop a movie using the given coordinates.

Performs y-coordinate flipping (GUI origin is bottom-left, numpy origin is top-left), sorts coordinates, and slices the movie array.

Parameters:

Name Type Description Default
movie movie

CaImAn movie to crop.

required
coords_dict Dict[str, int]

Dict with x0, y0, x1, y1 keys (in GUI coordinate space).

required

Returns:

Type Description
Tuple[movie, str]

Tuple of (cropped_movie, coords_string).

Source code in src/ace_neuro/miniscope/miniscope_preprocessor.py
def crop_movie(self, movie: cm.movie, coords_dict: Dict[str, int]) -> Tuple[cm.movie, str]:
    """Crop a movie using the given coordinates.

    Performs y-coordinate flipping (GUI origin is bottom-left, numpy
    origin is top-left), sorts coordinates, and slices the movie array.

    Args:
        movie: CaImAn movie to crop.
        coords_dict: Dict with x0, y0, x1, y1 keys (in GUI coordinate space).

    Returns:
        Tuple of (cropped_movie, coords_string).
    """
    # Flip y-coordinates (GUI origin is bottom-left; numpy origin is top-left)
    y0_flipped = movie.shape[1] - coords_dict['y1']
    y1_flipped = movie.shape[1] - coords_dict['y0']

    # Sort coordinates
    y0, y1 = sorted([y0_flipped, y1_flipped])
    x0, x1 = sorted([coords_dict['x0'], coords_dict['x1']])

    #crop movie using our numpy coordinates
    cropped_movie = movie[:, y0:y1, x0:x1]

    #Keep coords_dict in GUI notation so that it will display properly in the GUI if you want to view them again
    coords_string = f'({coords_dict["x0"]},{coords_dict["y0"]}, {coords_dict["x1"]},{coords_dict["y1"]})'

    return cropped_movie, coords_string

detrend_movie(movie, method='median', plot_trend=True)

Remove slow temporal trends from the movie.

Supports linear detrending or median-based debleaching to correct for photobleaching and other drift.

Parameters:

Name Type Description Default
movie movie

CaImAn movie to detrend.

required
method str

'linear' for scipy detrend, 'median' for CaImAn debleach.

'median'
plot_trend bool

If True, display before/after comparison plot.

True

Returns:

Type Description
movie

Detrended CaImAn movie.

Source code in src/ace_neuro/miniscope/miniscope_preprocessor.py
def detrend_movie(
    self, 
    movie: cm.movie, 
    method: str = 'median', 
    plot_trend: bool = True
) -> cm.movie:
    """Remove slow temporal trends from the movie.

    Supports linear detrending or median-based debleaching to correct
    for photobleaching and other drift.

    Args:
        movie: CaImAn movie to detrend.
        method: 'linear' for scipy detrend, 'median' for CaImAn debleach.
        plot_trend: If True, display before/after comparison plot.

    Returns:
        Detrended CaImAn movie.
    """
    detrended_movie = movie # Initialize with the original movie
    try:                    
        if method == 'linear':
            detrended_movie = detrend(movie, axis=0)
        elif method == 'median':
            # Manual median-based detrending (debleach was removed in newer CaIman versions)
            # Subtract the running median baseline to correct for photobleaching
            mean_trace = np.mean(movie, axis=(1, 2))
            median_baseline = np.median(mean_trace)
            trend = mean_trace - median_baseline
            # Subtract trend from each frame
            detrended_movie = movie - trend[:, np.newaxis, np.newaxis]
    except (ValueError, np.linalg.LinAlgError) as e:
        print(f"Detrending failed ({e}), returning original movie")
        return movie

    if plot_trend and detrended_movie is not None:
        fig, ax = plt.subplots()
        ax.set_xlabel('Frames')
        ax.set_ylabel('Mean Fluorescence')

        # Plot original data
        original_mean = np.mean(movie, axis=(1, 2))
        ax.plot(original_mean, label='Original Data', color='blue')

        # Plot detrended data
        detrended_mean = np.mean(detrended_movie, axis=(1, 2))
        ax.plot(detrended_mean, label='Detrended Data', color='red', linestyle='--')

        ax.legend()
        ax.grid(True)
        plt.tight_layout()
        plt.show()

    if isinstance(detrended_movie, np.ndarray):
        print("Ensuring that the movie that is returned is a caiman movie, not a numpy array...")
        detrended_movie = cm.movie(detrended_movie, fr=self.frame_rate)

    print('Detrending was successful')
    return detrended_movie

get_crop_coordinates(coords_dict, projections, movie_height, movie_width, headless=False)

Get crop coordinates from GUI or provided coordinates.

In headless mode, returns the provided coordinates directly without opening a GUI. If no coordinates are available in headless mode, returns None with a warning.

In interactive mode, opens crop GUI (pre-populated with coords_dict if available) for visual adjustment.

Parameters:

Name Type Description Default
coords_dict Optional[Dict[str, int]]

Dict with x0, y0, x1, y1 keys, or None for GUI.

required
projections Projections

Projections object for GUI visualization.

required
movie_height int

Height of the movie in pixels.

required
movie_width int

Width of the movie in pixels.

required
headless bool

If True, bypass GUI and use coords_dict directly.

False

Returns:

Type Description
Optional[Dict[str, int]]

Dict with x0, y0, x1, y1 keys, or None if no coordinates available.

Source code in src/ace_neuro/miniscope/miniscope_preprocessor.py
def get_crop_coordinates(
    self, 
    coords_dict: Optional[Dict[str, int]], 
    projections: Projections, 
    movie_height: int, 
    movie_width: int, 
    headless: bool = False
) -> Optional[Dict[str, int]]:
    """Get crop coordinates from GUI or provided coordinates.

    In headless mode, returns the provided coordinates directly without
    opening a GUI. If no coordinates are available in headless mode,
    returns None with a warning.

    In interactive mode, opens crop GUI (pre-populated with coords_dict
    if available) for visual adjustment.

    Args:
        coords_dict: Dict with x0, y0, x1, y1 keys, or None for GUI.
        projections: Projections object for GUI visualization.
        movie_height: Height of the movie in pixels.
        movie_width: Width of the movie in pixels.
        headless: If True, bypass GUI and use coords_dict directly.

    Returns:
        Dict with x0, y0, x1, y1 keys, or None if no coordinates available.
    """
    if headless:
        if coords_dict is None:
            print("WARNING: crop=True but no crop coordinates found in analysis_parameters.csv. "
                  "Skipping crop in headless mode. Provide 'crop' coordinates "
                  "in your analysis_parameters.csv to crop in headless mode.", flush=True)
            return None
        print(f"HEADLESS: Cropping with coordinates from analysis_parameters.csv: {coords_dict}", flush=True)
        return coords_dict
    else:
        return crop_gui(coords_dict, projections, movie_height, movie_width)

preprocess_calcium_movie(coords_dict=None, crop=False, detrend_method=None, df_over_f=False, crop_job_name_for_file='_cropped', secs_window=5, quantile_min=8, df_over_f_method='delta_f_over_sqrt_f', headless=False)

Run preprocessing steps based on provided flags. coords_dict: is passed in and represents what you want the final coordinates for the movie to be in the form {'x0': A, 'y0': B, 'x1': C, 'y1': D}

Source code in src/ace_neuro/miniscope/miniscope_preprocessor.py
def preprocess_calcium_movie(
    self, 
    coords_dict: Optional[Dict[str, int]] = None, 
    crop: bool = False, 
    detrend_method: Optional[str] = None, 
    df_over_f: bool = False, 
    crop_job_name_for_file: str = "_cropped",
    secs_window: float = 5, 
    quantile_min: float = 8, 
    df_over_f_method: str = 'delta_f_over_sqrt_f', 
    headless: bool = False
) -> 'MiniscopeDataManager':
    """Run preprocessing steps based on provided flags.
       coords_dict: is passed in and represents what you want the final coordinates for the movie to be in the form {'x0': A, 'y0': B, 'x1': C, 'y1': D}"""

    steps_applied = ['preprocessed']

    if crop:
        self.data_manager.projections = self.compute_projections(self.data_manager.movie)
        movie_height = self.data_manager.movie.shape[1]
        movie_width = self.data_manager.movie.shape[2]
        final_coords = self.get_crop_coordinates(coords_dict, self.data_manager.projections, movie_height, movie_width, headless=headless)
        if final_coords is not None:
            self.data_manager.movie, cropped_movie_filepath = self.crop_movie(self.data_manager.movie, final_coords)
            self.data_manager.preprocessed_movie_filepath = cropped_movie_filepath
            self.data_manager.coords = final_coords # Store the coordinates used

    if detrend_method:
        self.data_manager.movie = self.detrend_movie(self.data_manager.movie, method=detrend_method, plot_trend=not headless)
        steps_applied.append('_detrended')

    if df_over_f:
        self.data_manager.movie = self.compute_df_over_f(self.data_manager.movie, secs_window=secs_window, quantile_min=quantile_min, method=df_over_f_method)
        steps_applied.append('_dFoverF')

    movie_file_name = ''.join(steps_applied)

    self.data_manager.preprocessed_movie_filepath = MovieIO.save_movie(self.data_manager, movie_file_name)

    print(f"This is the movie shape after preprocessing: {self.data_manager.movie.shape}")

    return self.data_manager

Processor (CNMF-E)

ace_neuro.miniscope.miniscope_processor.MiniscopeProcessor

Main processor for calcium imaging movie analysis using CaImAn.

Orchestrates the complete analysis pipeline including motion correction, CNMF-E source extraction, and result saving. Works with MiniscopeDataManager to track all processing state and outputs.

Attributes:

Name Type Description
data_manager MiniscopeDataManager

MiniscopeDataManager containing movie and parameters.

preprocessed_movie movie

Copy of original movie before processing.

Source code in src/ace_neuro/miniscope/miniscope_processor.py
 21
 22
 23
 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
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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
232
233
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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
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
381
382
383
384
385
386
387
388
389
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
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
class MiniscopeProcessor:
    """Main processor for calcium imaging movie analysis using CaImAn.

    Orchestrates the complete analysis pipeline including motion correction,
    CNMF-E source extraction, and result saving. Works with MiniscopeDataManager
    to track all processing state and outputs.

    Attributes:
        data_manager: MiniscopeDataManager containing movie and parameters.
        preprocessed_movie: Copy of original movie before processing.
    """

    data_manager: MiniscopeDataManager
    preprocessed_movie: cm.movie

    def __init__(self, data_manager: MiniscopeDataManager) -> None:
        """
        Ensure that data_manager.movie contains the proper movie that you want to process before intializing this class

        This class has five main steps:
            1. Set up what processing type you would like to do the next steps with (parellel, how many cores, etc.)
            2. Motion correct (or don't) what is stored in data_manager.movie and save the result as a memory map
            3. Prepare/visualize different things to help you find optimal paramters for running CNMFE (an algorithm that decomposes a movie into multiple matrices)
            4. Run CNMFE using our memory map using the parameters stored in opts_caiman, which draws from analysis_parameters.csv
            5. Save any results

        """

        self.data_manager = data_manager
        self.preprocessed_movie = deepcopy(data_manager.movie)
        self._prepare_opts_caiman()


    def process_calcium_movie(
        self, 
        parallel: bool = True, 
        n_processes: int = 12, 
        apply_motion_correction: bool = True, 
        inspect_motion_correction: bool = False, 
        plot_params: bool = False, 
        run_CNMFE: bool = True,
        save_estimates: bool = True, 
        save_CNMFE_estimates_filename: str = 'estimates.hdf5', 
        save_CNMFE_params: bool = False
    ) -> MiniscopeDataManager:
        """Run the complete calcium movie processing pipeline.

        Executes motion correction, CNMF-E source extraction, and saves results.
        This is the main entry point for processing miniscope recordings.

        Args:
            parallel: If True, use multiprocessing for CaImAn operations.
            n_processes: Number of parallel processes to use.
            apply_motion_correction: If True, perform motion correction.
            inspect_motion_correction: If True, show motion correction diagnostics.
            plot_params: If True, display CNMF-E parameter tuning plots.
            run_CNMFE: If True, run CNMF-E source extraction algorithm.
            save_estimates: If True, save CNMF-E results to disk.
            save_CNMFE_estimates_filename: Filename for saved estimates.
            save_CNMFE_params: If True, save CaImAn parameters to JSON.

        Returns:
            Updated MiniscopeDataManager with processing results.
        """

        #set up processing type
        dview = None
        if parallel:
            print('Setting up cluster for caiman parallel processing on your computer')
            c, dview, n_processes = cm.cluster.setup_cluster(backend='multiprocessing', n_processes=n_processes, single_thread=False)

        #Apply motion correction, then saves a memory map to opts_caiman to prepare for CNMFE.
        self.data_manager = self.motion_correction_manager(self.data_manager, dview, apply_motion_correction, inspect_motion_correction)

        #Prepare additional analysis parameters for CNMFE
        self.data_manager, images = self.cnmfe_parameter_handler(self.data_manager, plot_params=plot_params)

        #intialize CNMFE object
        self.data_manager.CNMFE_obj = cm.source_extraction.cnmf.CNMF(n_processes=n_processes, dview=dview, Ain=None, params=self.data_manager.opts_caiman)

        #reupdate data_manager.movie with motion-corrected movie, otherwise the movie returned below is the same if you did not motion correct
        self.data_manager.movie = cm.movie(images, fr=self.data_manager.fr)

        #run CNMFE. Do not run unless you have optimal parameters or the neuron estimates will be junk
        if run_CNMFE:
            print('Running CNMFE...')
            try:
                self.data_manager.CNMFE_obj.fit(images)
            except (ValueError, MemoryError, RuntimeError) as e:
                print('CNMFE failed to run. Please check the parameters and try again.')
                print('No estimates were saved to disk. Do not continue to post-processing or multimodal analysis')
                raise ProcessingError(f"CNMFE failed: {e}") from e

        #save results 'estimates' to disk and update data_manager with their filepaths
        self.data_manager = self._save_processed_data(self.data_manager, save_estimates, save_CNMFE_estimates_filename, save_CNMFE_params)

        try:
            cm.stop_server(dview=dview)
        except (OSError, AttributeError) as e:
            print(f"Warning: could not stop CaImAn processing server: {e}")

        return self.data_manager





    def motion_correction_manager(
        self, 
        data_manager: MiniscopeDataManager, 
        dview: Any, 
        apply_motion_correction: bool, 
        inspect_motion_correction: bool
    ) -> MiniscopeDataManager:
        """Manage the motion correction workflow.

        Applies motion correction if requested, creates memory-mapped files,
        and optionally displays diagnostic visualizations.

        Args:
            data_manager: MiniscopeDataManager with movie data.
            dview: CaImAn distributed view object for parallel processing.
            apply_motion_correction: If True, perform motion correction.
            inspect_motion_correction: If True, show before/after comparisons.

        Returns:
            Updated data_manager with motion-corrected memory map.
        """
        motion_correction_object = None
        if apply_motion_correction:
            #apply motion correction
            motion_correction_object, data_manager.opts_caiman = self._apply_motion_correction(data_manager.opts_caiman, dview)
            #save the mmap file to disk and add that filepath to opts_caiman
            data_manager.opts_caiman = self._add_temp_mmap_to_opts_caiman(motion_correction_object.mmap_file, data_manager.opts_caiman, data_manager.opts_caiman.get('patch', 'border_pix'))
            #save the mmap file stored in motion_correction_object to data_manager
        else:
            #add our non-motion-corrected movie to opts_caiman to prepare for CNMFE
            data_manager.opts_caiman = self._add_temp_mmap_to_opts_caiman(data_manager.opts_caiman.get('data', 'fnames'), data_manager.opts_caiman, data_manager.opts_caiman.get('patch', 'border_pix'), dview)

        if inspect_motion_correction and apply_motion_correction and motion_correction_object is not None:
            self.inspect_motion_correction(motion_correction_object, data_manager.opts_caiman, self.preprocessed_movie, self.data_manager.fr)

        return data_manager

    def cleanup_tkinter(self) -> None:
        """Helper to cleanup tkinter root if it exists."""
        root = getattr(tkinter, '_default_root', None)
        if root:
            root.destroy()
        # Set Matplotlib backend to Qt5Agg for interactive plotting
        try:
            matplotlib.use('Qt5Agg')
            print("Matplotlib backend set to Qt5Agg")
        except Exception as e:
            print(f"Error setting Qt5Agg backend: {e}")
            matplotlib.use('Agg')  # Fallback to non-interactive backend

    def cnmfe_parameter_handler(self, dm: MiniscopeDataManager, plot_params: bool = False) -> Tuple[MiniscopeDataManager, np.ndarray]:
        """
        -This is an important step before CNMFE. It handles the most important CNMFE parameters.

        -It uses the 'gsig_tmp' below to make the first plot. Adjust this value until the plot shown does not merge any neurons, 
        then update 'gsig' in analysis_parameters.csv with the best 'gsig_tmp' value

        -First, this function calculates correlation and peak to noise ratios and plots them. Use the sliders to adjust 'vmax' until neurons are most visible, 
        then adjust 'min_corr' and 'min_pnr' in analysis_parameters.csv to values slightly below those vmax values
        Example: If neurons are most clear at vmax=0.9, set min_corr=0.8 or 0.85 (slightly below to capture most neurons)

        -Second, it plots your 'rf' and 'stride' values which form patches around your movie. We want to select rf and stride parameters so that 
        at least 3-4 neuron diameters can fit into each patch, and at least one neuron fits in the overlap region between patches.
        If patches and overlaps seem a bit large that is ok: our main concern is that they not be too small.
        """

        self.cleanup_tkinter()

        #load memory map and recompute movie from it
        Yr, dims, T = cm.load_memmap(dm.opts_caiman.get('data', 'fnames')[0])
        dm.opts_caiman.change_params({'data': {'dims': dims}})
        images = Yr.T.reshape((T,) + dims, order='F')


        if plot_params:
            #calculate min correlation/min pnr and plot them
            gsig_tmp = (3,3)
            correlation_image, peak_to_noise_ratio = cm.summary_images.correlation_pnr(images[::max(T//1000, 1)], gSig=gsig_tmp[0], swap_dim=False)
            if hasattr(dm, 'diag_logger') and dm.diag_logger is not None: dm.diag_logger.pause_timer()
            cm.utils.visualization.inspect_correlation_pnr(correlation_image, peak_to_noise_ratio)
            plt.show(block=True)
            if hasattr(dm, 'diag_logger') and dm.diag_logger is not None: dm.diag_logger.resume_timer()

            #Calculate stride/overlap and plot them
            cnmfe_patch_width = dm.opts_caiman.get('patch', 'rf') * 2 + 1
            cnmfe_patch_overlap = dm.opts_caiman.get('patch', 'stride') + 1
            cnmfe_patch_stride = cnmfe_patch_width - cnmfe_patch_overlap
            print(f'Patch width: {cnmfe_patch_width} , Stride: {cnmfe_patch_stride}, Overlap: {cnmfe_patch_overlap}')

            if hasattr(dm, 'diag_logger') and dm.diag_logger is not None: dm.diag_logger.pause_timer()
            patch_ax = cm.utils.visualization.view_quilt(correlation_image, cnmfe_patch_stride, cnmfe_patch_overlap, vmin=np.percentile(np.ravel(correlation_image), 50), 
                                                         vmax=np.percentile(np.ravel(correlation_image), 99.5), color='yellow', figsize=(4,4))
            patch_ax.set_title(f'CNMFE Patch Width {cnmfe_patch_width}, Overlap {cnmfe_patch_overlap}')
            plt.show(block=True)
            if hasattr(dm, 'diag_logger') and dm.diag_logger is not None: dm.diag_logger.resume_timer()

            #REMEMBER! Change analysis_parameter.csv so that these paramters are optimal BEFORE running CNMFE, then skip this step when they are optimal
        return dm, images


    def inspect_motion_correction(
        self, 
        mc: Any, 
        opts_caiman: 'CNMFParams', 
        original_movie: cm.movie, 
        frame_rate: float, 
        plot_rigid_motion_correction: bool = True, 
        plot_shifts: bool = True, 
        play_concatenated_movies: bool = True, 
        down_sample_ratio: float = 0.2, 
        plot_correlation: bool = True, 
        plot_advanced_MC_inspection: bool = True
    ) -> None:
        """This function is a mess and needs a lot of work. It does not work well at all.
        mc: the motion correction object obtained from apply_motion_correction()
        opts_caiman: a caiman parameters object obtained from cm.CNMFParams()
        original_movie: your movie jsut before motion_correction
        plot_rigid_motion_correction: a boolean that determines whether rigid motion correction is plotted.
        play_concatenated_movies: a boolean that determines whether the original and motion-corrected movies are plotted side-by-side.
        down_sample_ratio: a float that determines the factor by which to shrink the duration of the playback (helpful for making the motion more obvious).
        plot_shifts: a boolean that determines whether to plot the x and y pixel shifts over time.
        plot_correlation: a boolean that determines whether to plot the correlation images for the original and motion-corrected movies side-by-side.
        """
        print('Inspecting motion correction...')
        if plot_rigid_motion_correction:
            h, ax_any = misc_functions._prep_axes(xLabel=['', 'Frames'], yLabel=['', 'Pixels'], subPlots=[1, 2])
            ax = cast(List[Any], ax_any)
            ax[0].imshow(mc.total_template_rig)  # % plot template
            ax[1].plot(mc.shifts_rig)  # % plot rigid shifts
            ax[1].legend(['X Shifts', 'Y Shifts'])

        if plot_shifts:
            if opts_caiman.get('motion', 'pw_rigid'):
                h, ax_single = misc_functions._prep_axes(xLabel='Frames', yLabel='Pixels')
                if isinstance(ax_single, (list, np.ndarray)):
                    ax_plot = ax_single[0]
                else:
                    ax_plot = ax_single
                ax_plot.plot(mc.shifts_rig)
                ax_plot.legend(['X Shifts', 'Y Shifts'])
            else:
                h, ax_any = misc_functions._prep_axes(xLabel=['', 'Frames'],
                                                 yLabel=['X Shifts (Pixels)', 'Y Shifts (Pixels)'], subPlots=[2, 1])
                ax = cast(List[Any], ax_any)
                ax[0].plot(mc.x_shifts_els)
                ax[1].plot(mc.y_shifts_els)

        if play_concatenated_movies or plot_correlation:
            mc_movie = cm.load(mc.mmap_file)
            if play_concatenated_movies:
                print("WARNING! The concatenated clips being shown are different in brightness but still represent your movie before and after motion correction.")
                print("The old movie is the one that is not displaying properly, as it needed to be edited temporarily to fit the concatenation process")
                # Get dimensions of motion-corrected movie
                mc_height = mc_movie.shape[1]  # Height (dimension 1)
                mc_width = mc_movie.shape[2]   # Width (dimension 2)

                # Crop original movie to match mc_movie dimensions
                before_movie = original_movie[:, 0:mc_height, 0:mc_width]

                # Resize movies
                m1 = before_movie.resize(1, 1, down_sample_ratio)
                m2 = mc_movie.resize(1, 1, down_sample_ratio)

                # Handle NaN and inf values
                if np.any(np.isnan(m1)) or np.any(np.isinf(m1)):
                    print('Found NaN or inf values in the original movie...')
                    m1[np.isnan(m1) | np.isinf(m1)] = np.nanmean(m1[np.isfinite(m1)])
                if np.any(np.isnan(m2)) or np.any(np.isinf(m2)):
                    print('Found NaN or inf values in the motion-corrected movie...')
                    m2[np.isnan(m2) | np.isinf(m2)] = np.nanmean(m2[np.isfinite(m2)])

                # Clip negative values
                m1 = np.clip(m1, 0, None)
                m2 = np.clip(m2, 0, None)

                # Independent normalization
                m1 = (m1 - np.min(m1)) / (np.max(m1) - np.min(m1) + 1e-10)
                m2 = (m2 - np.nanmin(m2)) / (np.nanmax(m2) - np.nanmin(m2) + 1e-10)

                # Boost m1 brightness
                m1 = np.clip(m1 * 3, 0, 1)  # Adjust multiplier (e.g., 1.2 to 2.0) as needed

                # Concatenate and play
                if hasattr(self.data_manager, 'diag_logger') and self.data_manager.diag_logger is not None: self.data_manager.diag_logger.pause_timer()
                cm.concatenate([m1, m2], axis=2).play(fr=15, gain=1.0, magnification=2)
                if hasattr(self.data_manager, 'diag_logger') and self.data_manager.diag_logger is not None: self.data_manager.diag_logger.resume_timer()
                if play_concatenated_movies:
                    print("WARNING! The concatenated clips being shown are different in brightness but still represent your movie before and after motion correction.")
                    cm.concatenate([self.preprocessed_movie, mc_movie.resize(1, 1, down_sample_ratio)]).play(q_max=99.5, fr=frame_rate, magnification=2)

            if plot_correlation:
                h, ax_any = misc_functions._prep_axes(xLabel=['Original Movie', 'Motion Corrected Movie'], subPlots=[1, 2])
                ax = cast(List[Any], ax_any)
                ax[0].imshow(original_movie.local_correlations(eight_neighbours=True, swap_dim=False))
                ax[1].imshow(mc_movie.local_correlations(eight_neighbours=True, swap_dim=False))

        if plot_advanced_MC_inspection:
            final_size = np.subtract(opts_caiman.get('data', 'dims'),
                                     2 * mc.border_to_0)  # remove pixels in the boundaries
            winsize = 100
            swap_dim = False
            resize_fact_flow = .2  # downsample for computing ROF

            tmpl_orig, correlations_orig, flows_orig, norms_orig, crispness_orig = cm.motion_correction.compute_metrics_motion_correction(
                mc.fname[0], final_size[0], final_size[1], swap_dim, winsize=winsize, play_flow=False,
                resize_fact_flow=resize_fact_flow)

            tmpl_mc, correlations_mc, flows_mc, norms_mc, crispness_mc = cm.motion_correction.compute_metrics_motion_correction(
                mc.mmap_file[0], final_size[0], final_size[1],
                swap_dim, winsize=winsize, play_flow=False, resize_fact_flow=resize_fact_flow)

            if plot_correlation:
                fig, ax_any = plt.subplots(1, 2, sharex=True, sharey=True)
                ax = cast(List[Any], ax_any)
                ax[0].plot(correlations_orig)
                ax[0].plot(correlations_mc)
                ax[0].legend(['Original', 'Corrected'])
                ax[0].set_title('Correlations with Mean Frame')
                ax[1].scatter(correlations_orig, correlations_mc)
                ax[1].plot([0, 1], [0, 1], 'r--')
                ax[1].axis('square')
                ax[1].set_xlabel('Original Correlation')
                ax[1].set_ylabel('Corrected Correlation')
                ax[1].set_title('Correlation Comparison')

            # print crispness values
            print('Crispness original: ' + str(int(crispness_orig)))
            print('Crispness motion corrected: ' + str(int(crispness_mc)))

            # plot the results of Residual Optical Flow
            fls = [os.path.splitext(mc.fname[0])[0] + '_metrics.npz', os.path.splitext(mc.mmap_file[0])[0] + '_metrics.npz']
            fls = [os.path.splitext(mc.fname[0])[0] + '_metrics.npz', os.path.splitext(mc.mmap_file[0])[0] + '_metrics.npz']

            h, ax_any = misc_functions._prep_axes(title=['Mean', 'Corr Image', 'Mean Optical Flow', '', '', ''],
                                             xLabel=['Original', '', '', 'Motion Corrected', '', ''], yLabel=['', '', '', '', '', ''],
                                             subPlots=[2, 3])
            ax = cast(List[Any], ax_any)

            for cnt, fl in zip(range(len(fls)), fls):
                print(f"loading file into numpy: {fl}")
                with np.load(fl) as ld:
                    print(str(np.mean(ld['norms'])) + '+/-' + str(np.std(ld['norms'])) +
                          '; ' + str(ld['smoothness']) + '; ' + str(ld['smoothness_corr']))

                    if cnt == 0:
                        mean_img = np.mean(cm.load(mc.fname[0]), 0)[12:-12, 12:-12]
                    else:
                        mean_img = np.mean(cm.load(mc.mmap_file[0]), 0)[12:-12, 12:-12]

                    lq, hq = np.nanpercentile(mean_img, [0.5, 99.5])
                    ax[3 * cnt + 1].imshow(mean_img, vmin=lq, vmax=hq)
                    ax[3 * cnt + 2].imshow(ld['img_corr'], vmin=0, vmax=0.35)
                    # ax[3 * cnt + 3].plot(ld['norms'])
                    # ax[3 * cnt + 3].xlabel('frame')
                    # ax[3 * cnt + 3].ylabel('norm opt flow')
                    if len(ax) > (3 * cnt + 3):
                        mappable = ax[3 * cnt + 3].imshow(np.mean(
                            np.sqrt(ld['flows'][:, :, :, 0] ** 2 + ld['flows'][:, :, :, 1] ** 2), 0), vmin=0, vmax=0.3)
                        plt.colorbar(mappable=mappable, ax=ax[3 * cnt + 3]) #FIXME colorbar() is NOT an attribute of ax. It is of plt though"


    def _apply_motion_correction(self, opts_caiman: 'CNMFParams', dview: Any = None) -> Tuple[Any, 'CNMFParams']:
        """Motion corrects using a passed in caiman parameters object opts_caiman and calculates bord_px"""
        mc = cm.motion_correction.MotionCorrect(self.data_manager.preprocessed_movie_filepath, dview=dview, **opts_caiman.get_group('motion'))
        print(f"Motion correcting with these parameters: {opts_caiman.get_group('motion')}")

        #save_movie=True below saves a .npz file for the motion corrected movie to the same folder as self.movie_filepath, and allows us to save it as a mmap using method below this one
        mc.motion_correct(save_movie=True)
        if opts_caiman.get('motion', 'pw_rigid'):
            bord_px = np.ceil(np.maximum(np.max(np.abs(mc.x_shifts_els)), np.max(np.abs(mc.y_shifts_els)))).astype(int)
        else:
            bord_px = np.ceil(np.max(np.abs(mc.shifts_rig))).astype(int)
        bord_px = 0 if opts_caiman.get('motion', 'border_nan') == 'copy' else bord_px
        opts_caiman.change_params({'patch': {'border_pix': bord_px}})
        print(f'Updating border_pix with: {bord_px}')
        return mc, opts_caiman


    def _add_temp_mmap_to_opts_caiman(
        self, 
        filepath: Union[str, List[str], Path],
        opts_caiman: 'CNMFParams', 
        bord_px: int, 
        dview: Any = None
    ) -> 'CNMFParams':
        """Save movie to memory-mapped file and update CaImAn options.

        Creates a C-order memory-mapped file with border pixels set to zero,
        then updates opts_caiman to reference this file.

        Args:
            filepath: Path to movie file or existing mmap.
            opts_caiman: CaImAn parameters object to update.
            bord_px: Number of border pixels to set to zero.
            dview: Optional distributed view for parallel saving.

        Returns:
            Updated opts_caiman with new mmap filepath.
        """
        motion_corrected_mmap_filepath = cm.save_memmap(filepath, base_name="", order='C', border_to_0=bord_px, dview=dview)
        opts_caiman.change_params({'data': {'fnames': motion_corrected_mmap_filepath}})
        return opts_caiman


    def _save_processed_data(
        self, 
        dm: MiniscopeDataManager, 
        save_estimates: bool, 
        save_CNMFE_estimates_filename: str, 
        save_CNMFE_params: bool
    ) -> MiniscopeDataManager:
        """Save CNMF-E results and parameters to disk.

        Saves estimates to HDF5 and optionally saves CaImAn parameters to JSON.
        Updates data_manager with filepaths to saved files.

        Args:
            dm: MiniscopeDataManager with processing results.
            save_estimates: If True, save CNMF-E estimates.
            save_CNMFE_estimates_filename: Filename for estimates file.
            save_CNMFE_params: If True, save parameters to JSON.

        Returns:
            Updated data_manager with saved file paths.
        """
        if dm.metadata is not None:
            cal_imaging_dir = str(dm.metadata['calcium imaging directory'])
            save_dir = os.path.join(cal_imaging_dir, "saved_movies")
            os.makedirs(save_dir, exist_ok=True)

            if save_estimates and dm.CNMFE_obj is not None:
                CNMFE_obj_filepath = os.path.join(save_dir, str(save_CNMFE_estimates_filename))
                print('Saving CNMF-E estimates in ' + CNMFE_obj_filepath)
                dm.CNMFE_obj.save(CNMFE_obj_filepath) #saves the estimates from CNMFE to a file
                dm.estimates_filepath = CNMFE_obj_filepath

            if save_CNMFE_params and dm.opts_caiman is not None:
                opts_caiman_json_filepath = os.path.join(save_dir, "opts_caiman.json")
                print(f"Saving CaImAn params to {opts_caiman_json_filepath}")
                dm.opts_caiman.to_jsonfile(targfn=opts_caiman_json_filepath)
                dm.opts_caiman_filepath = opts_caiman_json_filepath

        return dm


    def _prepare_opts_caiman(self) -> None:
        """Prepare CaImAn parameters from analysis_params.

        Cleans and structures the flat analysis_params dictionary into
        the grouped format expected by CaImAn's CNMFParams. Removes
        experiment-specific keys and maps each parameter to its correct
        parameter group (data, patch, init, spatial, temporal, etc.).

        Returns:
            CaImAn CNMFParams object configured for CNMF-E.
        """
        #convert any analysis_params ending in .0 to integers, adds any needed params
        if self.data_manager.analysis_params is not None:
            #convert any analysis_params ending in .0 to integers, adds any needed params
            for key, value in self.data_manager.analysis_params.items():
                if isinstance(value, float) and value.is_integer():
                    self.data_manager.analysis_params[key] = int(value)

            print(f'updated dimensions in bottom with {self.data_manager.movie.shape[1:]}')
            self.data_manager.analysis_params['fnames'] = self.data_manager.preprocessed_movie_filepath
            self.data_manager.analysis_params['dims'] = self.data_manager.movie.shape[1:]
            self.data_manager.analysis_params['fr'] = self.data_manager.fr

        # Create a clean dictionary for CaImAn
        if self.data_manager.analysis_params is None:
            caiman_params = {}
        else:
            caiman_params = self.data_manager.analysis_params.copy()
        keys_to_remove = [
            'line number', 'id', 'date (YYMMDD)', 'Box calcium folder ID', 
            'calcium imaging directory', 'Box ephys folder ID', 'ephys directory', 
            'indices of TTL events to delete', 'zero time (s)', 'baseline period (min)', 
            'crop', 'crop_coords', 'periods of high slow wave power (s)', 'control periods (s)', 
            'ca_ephys_baseline_video_num', 'ca_ephys_slow_wave_video_num', 
            'ca_ephys_burst_suppression_video_num', 'comments'
        ]

        for key in keys_to_remove:
            caiman_params.pop(key, None)

        # Define parameter groups to map flat parameters to their respective groups
        # This prevents the "non-pathed parameters" deprecation warning in CaImAn
        structured_params = {}

        # Based on CaImAn CNMFParams groups
        param_groups = {
            'data': ['fnames', 'dims', 'fr', 'decay_time', 'dxy', 'var_name_hdf5', 'caiman_version', 'last_commit'],
            'patch': ['border_pix', 'del_duplicates', 'in_memory', 'low_rank_background', 'memory_fact', 'n_processes', 'nb_patch', 'only_init', 'p_patch', 'remove_very_bad_comps', 'rf', 'skip_refinement', 'p_ssub', 'stride', 'p_tsub'],
            'preprocess': ['check_nan', 'compute_g', 'include_noise', 'lags', 'max_num_samples_fft', 'n_pixels_per_process', 'noise_method', 'noise_range', 'p', 'pixels', 'sn'],
            'init': ['K', 'SC_kernel', 'SC_sigma', 'SC_thr', 'SC_normalize', 'SC_use_NN', 'SC_nnn', 'alpha_snmf', 'center_psf', 'gSig', 'gSiz', 'greedyroi_nmf_init_method', 'greedyroi_nmf_max_iter', 'init_iter', 'kernel', 'lambda_gnmf', 'snmf_l1_ratio', 'maxIter', 'max_iter_snmf', 'method_init', 'min_corr', 'min_pnr', 'nIter', 'nb', 'normalize_init', 'options_local_NMF', 'perc_baseline_snmf', 'ring_size_factor', 'rolling_length', 'rolling_sum', 'seed_method', 'sigma_smooth_snmf', 'ssub', 'ssub_B', 'tsub'],
            'spatial': ['dist', 'expandCore', 'extract_cc', 'maxthr', 'medw', 'method_exp', 'method_ls', 'n_pixels_per_process', 'normalize_yyt_one', 'nrgthr', 'num_blocks_per_run_spat', 'se', 'ss', 'thr_method', 'update_background_components'],
            'temporal': ['ITER', 'bas_nonneg', 'block_size_temp', 'fudge_factor', 'lags', 'optimize_g', 'method_deconvolution', 'noise_method', 'noise_range', 'num_blocks_per_run_temp', 'p', 's_min', 'solvers', 'verbosity'],
            'merging': ['do_merge', 'merge_thr', 'merge_parallel'],
            'quality': ['SNR_lowest', 'cnn_lowest', 'gSig_range', 'min_SNR', 'min_cnn_thr', 'rval_lowest', 'rval_thr', 'use_cnn', 'use_ecc', 'max_ecc'],
            'online': ['N_samples_exceptionality', 'batch_update_suff_stat', 'dist_shape_update', 'ds_factor', 'epochs', 'expected_comps', 'full_XXt', 'init_batch', 'init_method', 'iters_shape', 'max_comp_update_shape', 'max_num_added', 'max_shifts_online', 'min_SNR', 'min_num_trial', 'minibatch_shape', 'minibatch_suff_stat', 'motion_correct', 'movie_name_online', 'normalize', 'n_refit', 'num_times_comp_updated', 'opencv_codec', 'path_to_model', 'ring_CNN', 'rval_thr', 'save_online_movie', 'show_movie', 'simultaneously', 'sniper_mode', 'stop_detection', 'test_both', 'thresh_CNN_noisy', 'thresh_fitness_delta', 'thresh_fitness_raw', 'thresh_overlap', 'update_freq', 'update_num_comps', 'use_corr_img', 'use_dense', 'use_peak_max', 'W_update_factor'],
            'motion': ['border_nan', 'gSig_filt', 'is3D', 'max_deviation_rigid', 'max_shifts', 'min_mov', 'niter_rig', 'nonneg_movie', 'num_frames_split', 'num_splits_to_process_els', 'num_splits_to_process_rig', 'overlaps', 'pw_rigid', 'shifts_interpolate', 'shifts_opencv', 'splits_els', 'splits_rig', 'strides', 'upsample_factor_grid', 'use_cuda', 'indices'],
            'ring_CNN': ['n_channels', 'use_bias', 'use_add', 'pct', 'patience', 'max_epochs', 'width', 'loss_fn', 'lr', 'lr_scheduler', 'path_to_model', 'remove_activity', 'reuse_model']
        }

        # Reverse mapping for easy lookup
        key_to_groups = {}
        for group, keys in param_groups.items():
            for key in keys:
                if key not in key_to_groups:
                    key_to_groups[key] = []
                key_to_groups[key].append(group)

        for key, value in caiman_params.items():
            if key in key_to_groups:
                for group in key_to_groups[key]:
                    if group not in structured_params:
                        structured_params[group] = {}
                    structured_params[group][key] = value
            else:
                 # If parameter is not known, we can attempt to put it in 'data' or log a warning
                 print(f"Warning: Parameter '{key}' is not recognized in the standard CaImAn groups. It will be ignored.")

        self.data_manager.opts_caiman = cm.source_extraction.cnmf.params.CNMFParams(params_dict=structured_params) #intialize caiman CNMFParams object

__init__(data_manager)

Ensure that data_manager.movie contains the proper movie that you want to process before intializing this class

This class has five main steps
  1. Set up what processing type you would like to do the next steps with (parellel, how many cores, etc.)
  2. Motion correct (or don't) what is stored in data_manager.movie and save the result as a memory map
  3. Prepare/visualize different things to help you find optimal paramters for running CNMFE (an algorithm that decomposes a movie into multiple matrices)
  4. Run CNMFE using our memory map using the parameters stored in opts_caiman, which draws from analysis_parameters.csv
  5. Save any results
Source code in src/ace_neuro/miniscope/miniscope_processor.py
def __init__(self, data_manager: MiniscopeDataManager) -> None:
    """
    Ensure that data_manager.movie contains the proper movie that you want to process before intializing this class

    This class has five main steps:
        1. Set up what processing type you would like to do the next steps with (parellel, how many cores, etc.)
        2. Motion correct (or don't) what is stored in data_manager.movie and save the result as a memory map
        3. Prepare/visualize different things to help you find optimal paramters for running CNMFE (an algorithm that decomposes a movie into multiple matrices)
        4. Run CNMFE using our memory map using the parameters stored in opts_caiman, which draws from analysis_parameters.csv
        5. Save any results

    """

    self.data_manager = data_manager
    self.preprocessed_movie = deepcopy(data_manager.movie)
    self._prepare_opts_caiman()

cleanup_tkinter()

Helper to cleanup tkinter root if it exists.

Source code in src/ace_neuro/miniscope/miniscope_processor.py
def cleanup_tkinter(self) -> None:
    """Helper to cleanup tkinter root if it exists."""
    root = getattr(tkinter, '_default_root', None)
    if root:
        root.destroy()
    # Set Matplotlib backend to Qt5Agg for interactive plotting
    try:
        matplotlib.use('Qt5Agg')
        print("Matplotlib backend set to Qt5Agg")
    except Exception as e:
        print(f"Error setting Qt5Agg backend: {e}")
        matplotlib.use('Agg')  # Fallback to non-interactive backend

cnmfe_parameter_handler(dm, plot_params=False)

-This is an important step before CNMFE. It handles the most important CNMFE parameters.

-It uses the 'gsig_tmp' below to make the first plot. Adjust this value until the plot shown does not merge any neurons, then update 'gsig' in analysis_parameters.csv with the best 'gsig_tmp' value

-First, this function calculates correlation and peak to noise ratios and plots them. Use the sliders to adjust 'vmax' until neurons are most visible, then adjust 'min_corr' and 'min_pnr' in analysis_parameters.csv to values slightly below those vmax values Example: If neurons are most clear at vmax=0.9, set min_corr=0.8 or 0.85 (slightly below to capture most neurons)

-Second, it plots your 'rf' and 'stride' values which form patches around your movie. We want to select rf and stride parameters so that at least 3-4 neuron diameters can fit into each patch, and at least one neuron fits in the overlap region between patches. If patches and overlaps seem a bit large that is ok: our main concern is that they not be too small.

Source code in src/ace_neuro/miniscope/miniscope_processor.py
def cnmfe_parameter_handler(self, dm: MiniscopeDataManager, plot_params: bool = False) -> Tuple[MiniscopeDataManager, np.ndarray]:
    """
    -This is an important step before CNMFE. It handles the most important CNMFE parameters.

    -It uses the 'gsig_tmp' below to make the first plot. Adjust this value until the plot shown does not merge any neurons, 
    then update 'gsig' in analysis_parameters.csv with the best 'gsig_tmp' value

    -First, this function calculates correlation and peak to noise ratios and plots them. Use the sliders to adjust 'vmax' until neurons are most visible, 
    then adjust 'min_corr' and 'min_pnr' in analysis_parameters.csv to values slightly below those vmax values
    Example: If neurons are most clear at vmax=0.9, set min_corr=0.8 or 0.85 (slightly below to capture most neurons)

    -Second, it plots your 'rf' and 'stride' values which form patches around your movie. We want to select rf and stride parameters so that 
    at least 3-4 neuron diameters can fit into each patch, and at least one neuron fits in the overlap region between patches.
    If patches and overlaps seem a bit large that is ok: our main concern is that they not be too small.
    """

    self.cleanup_tkinter()

    #load memory map and recompute movie from it
    Yr, dims, T = cm.load_memmap(dm.opts_caiman.get('data', 'fnames')[0])
    dm.opts_caiman.change_params({'data': {'dims': dims}})
    images = Yr.T.reshape((T,) + dims, order='F')


    if plot_params:
        #calculate min correlation/min pnr and plot them
        gsig_tmp = (3,3)
        correlation_image, peak_to_noise_ratio = cm.summary_images.correlation_pnr(images[::max(T//1000, 1)], gSig=gsig_tmp[0], swap_dim=False)
        if hasattr(dm, 'diag_logger') and dm.diag_logger is not None: dm.diag_logger.pause_timer()
        cm.utils.visualization.inspect_correlation_pnr(correlation_image, peak_to_noise_ratio)
        plt.show(block=True)
        if hasattr(dm, 'diag_logger') and dm.diag_logger is not None: dm.diag_logger.resume_timer()

        #Calculate stride/overlap and plot them
        cnmfe_patch_width = dm.opts_caiman.get('patch', 'rf') * 2 + 1
        cnmfe_patch_overlap = dm.opts_caiman.get('patch', 'stride') + 1
        cnmfe_patch_stride = cnmfe_patch_width - cnmfe_patch_overlap
        print(f'Patch width: {cnmfe_patch_width} , Stride: {cnmfe_patch_stride}, Overlap: {cnmfe_patch_overlap}')

        if hasattr(dm, 'diag_logger') and dm.diag_logger is not None: dm.diag_logger.pause_timer()
        patch_ax = cm.utils.visualization.view_quilt(correlation_image, cnmfe_patch_stride, cnmfe_patch_overlap, vmin=np.percentile(np.ravel(correlation_image), 50), 
                                                     vmax=np.percentile(np.ravel(correlation_image), 99.5), color='yellow', figsize=(4,4))
        patch_ax.set_title(f'CNMFE Patch Width {cnmfe_patch_width}, Overlap {cnmfe_patch_overlap}')
        plt.show(block=True)
        if hasattr(dm, 'diag_logger') and dm.diag_logger is not None: dm.diag_logger.resume_timer()

        #REMEMBER! Change analysis_parameter.csv so that these paramters are optimal BEFORE running CNMFE, then skip this step when they are optimal
    return dm, images

inspect_motion_correction(mc, opts_caiman, original_movie, frame_rate, plot_rigid_motion_correction=True, plot_shifts=True, play_concatenated_movies=True, down_sample_ratio=0.2, plot_correlation=True, plot_advanced_MC_inspection=True)

This function is a mess and needs a lot of work. It does not work well at all. mc: the motion correction object obtained from apply_motion_correction() opts_caiman: a caiman parameters object obtained from cm.CNMFParams() original_movie: your movie jsut before motion_correction plot_rigid_motion_correction: a boolean that determines whether rigid motion correction is plotted. play_concatenated_movies: a boolean that determines whether the original and motion-corrected movies are plotted side-by-side. down_sample_ratio: a float that determines the factor by which to shrink the duration of the playback (helpful for making the motion more obvious). plot_shifts: a boolean that determines whether to plot the x and y pixel shifts over time. plot_correlation: a boolean that determines whether to plot the correlation images for the original and motion-corrected movies side-by-side.

Source code in src/ace_neuro/miniscope/miniscope_processor.py
def inspect_motion_correction(
    self, 
    mc: Any, 
    opts_caiman: 'CNMFParams', 
    original_movie: cm.movie, 
    frame_rate: float, 
    plot_rigid_motion_correction: bool = True, 
    plot_shifts: bool = True, 
    play_concatenated_movies: bool = True, 
    down_sample_ratio: float = 0.2, 
    plot_correlation: bool = True, 
    plot_advanced_MC_inspection: bool = True
) -> None:
    """This function is a mess and needs a lot of work. It does not work well at all.
    mc: the motion correction object obtained from apply_motion_correction()
    opts_caiman: a caiman parameters object obtained from cm.CNMFParams()
    original_movie: your movie jsut before motion_correction
    plot_rigid_motion_correction: a boolean that determines whether rigid motion correction is plotted.
    play_concatenated_movies: a boolean that determines whether the original and motion-corrected movies are plotted side-by-side.
    down_sample_ratio: a float that determines the factor by which to shrink the duration of the playback (helpful for making the motion more obvious).
    plot_shifts: a boolean that determines whether to plot the x and y pixel shifts over time.
    plot_correlation: a boolean that determines whether to plot the correlation images for the original and motion-corrected movies side-by-side.
    """
    print('Inspecting motion correction...')
    if plot_rigid_motion_correction:
        h, ax_any = misc_functions._prep_axes(xLabel=['', 'Frames'], yLabel=['', 'Pixels'], subPlots=[1, 2])
        ax = cast(List[Any], ax_any)
        ax[0].imshow(mc.total_template_rig)  # % plot template
        ax[1].plot(mc.shifts_rig)  # % plot rigid shifts
        ax[1].legend(['X Shifts', 'Y Shifts'])

    if plot_shifts:
        if opts_caiman.get('motion', 'pw_rigid'):
            h, ax_single = misc_functions._prep_axes(xLabel='Frames', yLabel='Pixels')
            if isinstance(ax_single, (list, np.ndarray)):
                ax_plot = ax_single[0]
            else:
                ax_plot = ax_single
            ax_plot.plot(mc.shifts_rig)
            ax_plot.legend(['X Shifts', 'Y Shifts'])
        else:
            h, ax_any = misc_functions._prep_axes(xLabel=['', 'Frames'],
                                             yLabel=['X Shifts (Pixels)', 'Y Shifts (Pixels)'], subPlots=[2, 1])
            ax = cast(List[Any], ax_any)
            ax[0].plot(mc.x_shifts_els)
            ax[1].plot(mc.y_shifts_els)

    if play_concatenated_movies or plot_correlation:
        mc_movie = cm.load(mc.mmap_file)
        if play_concatenated_movies:
            print("WARNING! The concatenated clips being shown are different in brightness but still represent your movie before and after motion correction.")
            print("The old movie is the one that is not displaying properly, as it needed to be edited temporarily to fit the concatenation process")
            # Get dimensions of motion-corrected movie
            mc_height = mc_movie.shape[1]  # Height (dimension 1)
            mc_width = mc_movie.shape[2]   # Width (dimension 2)

            # Crop original movie to match mc_movie dimensions
            before_movie = original_movie[:, 0:mc_height, 0:mc_width]

            # Resize movies
            m1 = before_movie.resize(1, 1, down_sample_ratio)
            m2 = mc_movie.resize(1, 1, down_sample_ratio)

            # Handle NaN and inf values
            if np.any(np.isnan(m1)) or np.any(np.isinf(m1)):
                print('Found NaN or inf values in the original movie...')
                m1[np.isnan(m1) | np.isinf(m1)] = np.nanmean(m1[np.isfinite(m1)])
            if np.any(np.isnan(m2)) or np.any(np.isinf(m2)):
                print('Found NaN or inf values in the motion-corrected movie...')
                m2[np.isnan(m2) | np.isinf(m2)] = np.nanmean(m2[np.isfinite(m2)])

            # Clip negative values
            m1 = np.clip(m1, 0, None)
            m2 = np.clip(m2, 0, None)

            # Independent normalization
            m1 = (m1 - np.min(m1)) / (np.max(m1) - np.min(m1) + 1e-10)
            m2 = (m2 - np.nanmin(m2)) / (np.nanmax(m2) - np.nanmin(m2) + 1e-10)

            # Boost m1 brightness
            m1 = np.clip(m1 * 3, 0, 1)  # Adjust multiplier (e.g., 1.2 to 2.0) as needed

            # Concatenate and play
            if hasattr(self.data_manager, 'diag_logger') and self.data_manager.diag_logger is not None: self.data_manager.diag_logger.pause_timer()
            cm.concatenate([m1, m2], axis=2).play(fr=15, gain=1.0, magnification=2)
            if hasattr(self.data_manager, 'diag_logger') and self.data_manager.diag_logger is not None: self.data_manager.diag_logger.resume_timer()
            if play_concatenated_movies:
                print("WARNING! The concatenated clips being shown are different in brightness but still represent your movie before and after motion correction.")
                cm.concatenate([self.preprocessed_movie, mc_movie.resize(1, 1, down_sample_ratio)]).play(q_max=99.5, fr=frame_rate, magnification=2)

        if plot_correlation:
            h, ax_any = misc_functions._prep_axes(xLabel=['Original Movie', 'Motion Corrected Movie'], subPlots=[1, 2])
            ax = cast(List[Any], ax_any)
            ax[0].imshow(original_movie.local_correlations(eight_neighbours=True, swap_dim=False))
            ax[1].imshow(mc_movie.local_correlations(eight_neighbours=True, swap_dim=False))

    if plot_advanced_MC_inspection:
        final_size = np.subtract(opts_caiman.get('data', 'dims'),
                                 2 * mc.border_to_0)  # remove pixels in the boundaries
        winsize = 100
        swap_dim = False
        resize_fact_flow = .2  # downsample for computing ROF

        tmpl_orig, correlations_orig, flows_orig, norms_orig, crispness_orig = cm.motion_correction.compute_metrics_motion_correction(
            mc.fname[0], final_size[0], final_size[1], swap_dim, winsize=winsize, play_flow=False,
            resize_fact_flow=resize_fact_flow)

        tmpl_mc, correlations_mc, flows_mc, norms_mc, crispness_mc = cm.motion_correction.compute_metrics_motion_correction(
            mc.mmap_file[0], final_size[0], final_size[1],
            swap_dim, winsize=winsize, play_flow=False, resize_fact_flow=resize_fact_flow)

        if plot_correlation:
            fig, ax_any = plt.subplots(1, 2, sharex=True, sharey=True)
            ax = cast(List[Any], ax_any)
            ax[0].plot(correlations_orig)
            ax[0].plot(correlations_mc)
            ax[0].legend(['Original', 'Corrected'])
            ax[0].set_title('Correlations with Mean Frame')
            ax[1].scatter(correlations_orig, correlations_mc)
            ax[1].plot([0, 1], [0, 1], 'r--')
            ax[1].axis('square')
            ax[1].set_xlabel('Original Correlation')
            ax[1].set_ylabel('Corrected Correlation')
            ax[1].set_title('Correlation Comparison')

        # print crispness values
        print('Crispness original: ' + str(int(crispness_orig)))
        print('Crispness motion corrected: ' + str(int(crispness_mc)))

        # plot the results of Residual Optical Flow
        fls = [os.path.splitext(mc.fname[0])[0] + '_metrics.npz', os.path.splitext(mc.mmap_file[0])[0] + '_metrics.npz']
        fls = [os.path.splitext(mc.fname[0])[0] + '_metrics.npz', os.path.splitext(mc.mmap_file[0])[0] + '_metrics.npz']

        h, ax_any = misc_functions._prep_axes(title=['Mean', 'Corr Image', 'Mean Optical Flow', '', '', ''],
                                         xLabel=['Original', '', '', 'Motion Corrected', '', ''], yLabel=['', '', '', '', '', ''],
                                         subPlots=[2, 3])
        ax = cast(List[Any], ax_any)

        for cnt, fl in zip(range(len(fls)), fls):
            print(f"loading file into numpy: {fl}")
            with np.load(fl) as ld:
                print(str(np.mean(ld['norms'])) + '+/-' + str(np.std(ld['norms'])) +
                      '; ' + str(ld['smoothness']) + '; ' + str(ld['smoothness_corr']))

                if cnt == 0:
                    mean_img = np.mean(cm.load(mc.fname[0]), 0)[12:-12, 12:-12]
                else:
                    mean_img = np.mean(cm.load(mc.mmap_file[0]), 0)[12:-12, 12:-12]

                lq, hq = np.nanpercentile(mean_img, [0.5, 99.5])
                ax[3 * cnt + 1].imshow(mean_img, vmin=lq, vmax=hq)
                ax[3 * cnt + 2].imshow(ld['img_corr'], vmin=0, vmax=0.35)
                # ax[3 * cnt + 3].plot(ld['norms'])
                # ax[3 * cnt + 3].xlabel('frame')
                # ax[3 * cnt + 3].ylabel('norm opt flow')
                if len(ax) > (3 * cnt + 3):
                    mappable = ax[3 * cnt + 3].imshow(np.mean(
                        np.sqrt(ld['flows'][:, :, :, 0] ** 2 + ld['flows'][:, :, :, 1] ** 2), 0), vmin=0, vmax=0.3)
                    plt.colorbar(mappable=mappable, ax=ax[3 * cnt + 3]) #FIXME colorbar() is NOT an attribute of ax. It is of plt though"

motion_correction_manager(data_manager, dview, apply_motion_correction, inspect_motion_correction)

Manage the motion correction workflow.

Applies motion correction if requested, creates memory-mapped files, and optionally displays diagnostic visualizations.

Parameters:

Name Type Description Default
data_manager MiniscopeDataManager

MiniscopeDataManager with movie data.

required
dview Any

CaImAn distributed view object for parallel processing.

required
apply_motion_correction bool

If True, perform motion correction.

required
inspect_motion_correction bool

If True, show before/after comparisons.

required

Returns:

Type Description
MiniscopeDataManager

Updated data_manager with motion-corrected memory map.

Source code in src/ace_neuro/miniscope/miniscope_processor.py
def motion_correction_manager(
    self, 
    data_manager: MiniscopeDataManager, 
    dview: Any, 
    apply_motion_correction: bool, 
    inspect_motion_correction: bool
) -> MiniscopeDataManager:
    """Manage the motion correction workflow.

    Applies motion correction if requested, creates memory-mapped files,
    and optionally displays diagnostic visualizations.

    Args:
        data_manager: MiniscopeDataManager with movie data.
        dview: CaImAn distributed view object for parallel processing.
        apply_motion_correction: If True, perform motion correction.
        inspect_motion_correction: If True, show before/after comparisons.

    Returns:
        Updated data_manager with motion-corrected memory map.
    """
    motion_correction_object = None
    if apply_motion_correction:
        #apply motion correction
        motion_correction_object, data_manager.opts_caiman = self._apply_motion_correction(data_manager.opts_caiman, dview)
        #save the mmap file to disk and add that filepath to opts_caiman
        data_manager.opts_caiman = self._add_temp_mmap_to_opts_caiman(motion_correction_object.mmap_file, data_manager.opts_caiman, data_manager.opts_caiman.get('patch', 'border_pix'))
        #save the mmap file stored in motion_correction_object to data_manager
    else:
        #add our non-motion-corrected movie to opts_caiman to prepare for CNMFE
        data_manager.opts_caiman = self._add_temp_mmap_to_opts_caiman(data_manager.opts_caiman.get('data', 'fnames'), data_manager.opts_caiman, data_manager.opts_caiman.get('patch', 'border_pix'), dview)

    if inspect_motion_correction and apply_motion_correction and motion_correction_object is not None:
        self.inspect_motion_correction(motion_correction_object, data_manager.opts_caiman, self.preprocessed_movie, self.data_manager.fr)

    return data_manager

process_calcium_movie(parallel=True, n_processes=12, apply_motion_correction=True, inspect_motion_correction=False, plot_params=False, run_CNMFE=True, save_estimates=True, save_CNMFE_estimates_filename='estimates.hdf5', save_CNMFE_params=False)

Run the complete calcium movie processing pipeline.

Executes motion correction, CNMF-E source extraction, and saves results. This is the main entry point for processing miniscope recordings.

Parameters:

Name Type Description Default
parallel bool

If True, use multiprocessing for CaImAn operations.

True
n_processes int

Number of parallel processes to use.

12
apply_motion_correction bool

If True, perform motion correction.

True
inspect_motion_correction bool

If True, show motion correction diagnostics.

False
plot_params bool

If True, display CNMF-E parameter tuning plots.

False
run_CNMFE bool

If True, run CNMF-E source extraction algorithm.

True
save_estimates bool

If True, save CNMF-E results to disk.

True
save_CNMFE_estimates_filename str

Filename for saved estimates.

'estimates.hdf5'
save_CNMFE_params bool

If True, save CaImAn parameters to JSON.

False

Returns:

Type Description
MiniscopeDataManager

Updated MiniscopeDataManager with processing results.

Source code in src/ace_neuro/miniscope/miniscope_processor.py
def process_calcium_movie(
    self, 
    parallel: bool = True, 
    n_processes: int = 12, 
    apply_motion_correction: bool = True, 
    inspect_motion_correction: bool = False, 
    plot_params: bool = False, 
    run_CNMFE: bool = True,
    save_estimates: bool = True, 
    save_CNMFE_estimates_filename: str = 'estimates.hdf5', 
    save_CNMFE_params: bool = False
) -> MiniscopeDataManager:
    """Run the complete calcium movie processing pipeline.

    Executes motion correction, CNMF-E source extraction, and saves results.
    This is the main entry point for processing miniscope recordings.

    Args:
        parallel: If True, use multiprocessing for CaImAn operations.
        n_processes: Number of parallel processes to use.
        apply_motion_correction: If True, perform motion correction.
        inspect_motion_correction: If True, show motion correction diagnostics.
        plot_params: If True, display CNMF-E parameter tuning plots.
        run_CNMFE: If True, run CNMF-E source extraction algorithm.
        save_estimates: If True, save CNMF-E results to disk.
        save_CNMFE_estimates_filename: Filename for saved estimates.
        save_CNMFE_params: If True, save CaImAn parameters to JSON.

    Returns:
        Updated MiniscopeDataManager with processing results.
    """

    #set up processing type
    dview = None
    if parallel:
        print('Setting up cluster for caiman parallel processing on your computer')
        c, dview, n_processes = cm.cluster.setup_cluster(backend='multiprocessing', n_processes=n_processes, single_thread=False)

    #Apply motion correction, then saves a memory map to opts_caiman to prepare for CNMFE.
    self.data_manager = self.motion_correction_manager(self.data_manager, dview, apply_motion_correction, inspect_motion_correction)

    #Prepare additional analysis parameters for CNMFE
    self.data_manager, images = self.cnmfe_parameter_handler(self.data_manager, plot_params=plot_params)

    #intialize CNMFE object
    self.data_manager.CNMFE_obj = cm.source_extraction.cnmf.CNMF(n_processes=n_processes, dview=dview, Ain=None, params=self.data_manager.opts_caiman)

    #reupdate data_manager.movie with motion-corrected movie, otherwise the movie returned below is the same if you did not motion correct
    self.data_manager.movie = cm.movie(images, fr=self.data_manager.fr)

    #run CNMFE. Do not run unless you have optimal parameters or the neuron estimates will be junk
    if run_CNMFE:
        print('Running CNMFE...')
        try:
            self.data_manager.CNMFE_obj.fit(images)
        except (ValueError, MemoryError, RuntimeError) as e:
            print('CNMFE failed to run. Please check the parameters and try again.')
            print('No estimates were saved to disk. Do not continue to post-processing or multimodal analysis')
            raise ProcessingError(f"CNMFE failed: {e}") from e

    #save results 'estimates' to disk and update data_manager with their filepaths
    self.data_manager = self._save_processed_data(self.data_manager, save_estimates, save_CNMFE_estimates_filename, save_CNMFE_params)

    try:
        cm.stop_server(dview=dview)
    except (OSError, AttributeError) as e:
        print(f"Warning: could not stop CaImAn processing server: {e}")

    return self.data_manager

Postprocessor

ace_neuro.miniscope.miniscope_postprocessor.MiniscopePostprocessor

Post-processor for CNMF-E extracted calcium imaging components.

Performs component refinement, calcium event detection, spectral analysis, and phase computation on processed miniscope data.

Attributes:

Name Type Description
data_manager MiniscopeDataManager

MiniscopeDataManager with CNMFE results.

frame_rate float

Recording frame rate from data_manager.

dview Any

CaImAn distributed view for parallel processing.

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
class MiniscopePostprocessor:
    """Post-processor for CNMF-E extracted calcium imaging components.

    Performs component refinement, calcium event detection, spectral analysis,
    and phase computation on processed miniscope data.

    Attributes:
        data_manager: MiniscopeDataManager with CNMFE results.
        frame_rate: Recording frame rate from data_manager.
        dview: CaImAn distributed view for parallel processing.
    """

    data_manager: 'MiniscopeDataManager'
    frame_rate: float
    dview: Any

    def __init__(self, data_manager: 'MiniscopeDataManager') -> None:
        """Initialize post-processor with data manager.

        Automatically computes movie projections on initialization.

        Args:
            data_manager: MiniscopeDataManager with movie and CNMFE results.
        """
        self.data_manager = data_manager
        self.data_manager.projections = self.compute_projections(self.data_manager.movie)
        self.frame_rate = float(self.data_manager.fr)
        self.dview = self.data_manager.dview


    def postprocess_calcium_movie(
        self, 
        remove_components_with_gui: bool = True,  
        find_calcium_events: bool = True,
        derivative_for_estimates: str = 'first', 
        event_height: float = 5, 
        compute_miniscope_phase: bool = True, 
        filter_miniscope_data: bool = True,
        n: int = 2, 
        cut: List[float] = [0.1, 1.5], 
        ftype: str = 'butter', 
        btype: str = 'bandpass', 
        inline: bool = False,
        compute_miniscope_spectrogram: bool = True,
        window_length: float = 30, 
        window_step: float = 3, 
        freq_lims: List[float] = [0, 15], 
        time_bandwidth: float = 2
    ) -> 'MiniscopeDataManager':
        """Run the complete post-processing pipeline on CNMF-E results.

        Performs component curation via GUI, calcium event detection, phase
        computation, filtering, and spectral analysis.

        Args:
            remove_components_with_gui: If True, open interactive GUI for component selection.
            find_calcium_events: If True, detect calcium transient events.
            derivative_for_estimates: Derivative order for event detection ('zeroth', 'first', 'second').
            event_height: Threshold height for peak detection.
            compute_miniscope_phase: If True, compute instantaneous phase via Hilbert.
            filter_miniscope_data: If True, apply bandpass filter to projections.
            n: Filter order.
            cut: [low, high] cutoff frequencies for bandpass.
            ftype: Filter type ('butter', 'fir').
            btype: Band type for filter.
            inline: If True, replace original data with filtered.
            compute_miniscope_spectrogram: If True, compute multitaper spectrogram.
            window_length: Spectrogram window length in seconds.
            window_step: Spectrogram step size in seconds.
            freq_lims: [low, high] frequency limits for spectrogram.
            time_bandwidth: Time-bandwidth product for multitaper.

        Returns:
            Updated MiniscopeDataManager with all post-processing results.
        """

        if remove_components_with_gui:
            if self.data_manager.CNMFE_obj is not None and self.data_manager.CNMFE_obj.estimates.A is not None and self.data_manager.CNMFE_obj.estimates.A.shape[0] > 0:
                if hasattr(self.data_manager, 'diag_logger') and self.data_manager.diag_logger is not None: self.data_manager.diag_logger.pause_timer()
                self.data_manager.CNMFE_obj.estimates.plot_contours()
                self.data_manager.CNMFE_obj.estimates = component_gui(self.data_manager.movie, self.data_manager.CNMFE_obj.estimates, self.data_manager.projections)
                if hasattr(self.data_manager, 'diag_logger') and self.data_manager.diag_logger is not None: self.data_manager.diag_logger.resume_timer()
            else:
                print("No components found or CNMF-E object is None. Skipping component GUI.")

        if find_calcium_events:
            if self.data_manager.CNMFE_obj is not None and self.data_manager.CNMFE_obj.estimates.C is not None:
                self.data_manager.ca_events_idx = self.find_calcium_events_with_derivatives(self.data_manager.CNMFE_obj.estimates, derivative_for_estimates, event_height)
            else:
                print("WARNING: No CNMF-E components found (estimates.C is None). Skipping calcium event detection.")
                self.data_manager.ca_events_idx = {}

        if compute_miniscope_spectrogram:
            data = self.data_manager.projections.time
            PSDSpectMiniscope, tSpect, freqsSpect, pSpectMiniscope = self.compute_miniscope_spectrogram(data, frame_rate=self.frame_rate, window_length=window_length, window_step=window_step, freq_lims=freq_lims, time_bandwidth=time_bandwidth)
            h, ax = misc_functions.spectrogram(tSpect/60, freqsSpect, pSpectMiniscope, xLabel='Time (min)')
            self.data_manager.PSD_spect, self.data_manager.t_spect, self.data_manager.freqs_spect, self.data_manager.p_spect = PSDSpectMiniscope, tSpect, freqsSpect, pSpectMiniscope

        if compute_miniscope_phase:
            self.data_manager.miniscope_phases = self.compute_miniscope_phase(self.data_manager.projections.time)

        if filter_miniscope_data:
            filter_object = FilterMiniscopeData(self.data_manager.projections, self.frame_rate, n=n, cut=cut, ftype=ftype, btype=btype)
            filter_object.filter_miniscope_data
            self.data_manager.filter_object = filter_object

            if inline == True:
                self.data_manager.projections.time = filter_object.filtered_data

        return self.data_manager



    def compute_projections(self, movie: Optional[cm.movie] = None) -> Projections:
        """Compute spatial and temporal projections of the movie.

        Calculates max, min, mean, median, std, range projections and
        mean fluorescence time series.

        Args:
            movie: CaImAn movie object to compute projections from.

        Returns:
            Projections object containing all computed projections.
        """
        print("\n\nComputing projections...\n")

        operations = {
            'max': lambda m: np.amax(m, axis=0),
            'std': lambda m: np.std(m, axis=0),
            'min': lambda m: np.amin(m, axis=0),
            'mean': lambda m: np.mean(m, axis=0),
            'median': lambda m: np.median(m, axis=0),
            'time': lambda m: m.mean(axis=(1,2)),
        }

        results = {}
        for name, op in tqdm(operations.items(), desc='Computing Projections'):
            results[name] = op(movie)

        results['range'] = results['max'] - results['min']

        return Projections(
            results['max'],
            results['std'],
            results['min'],
            results['mean'],
            results['median'],
            results['range'],
            results['time']
        )


    def evaluate_components(
        self, 
        estimates: 'Estimates', 
        opts_caiman: 'CNMFParams', 
        min_SNR: float = 3, 
        r_values_min: float = 0.85
    ) -> Tuple['Estimates', 'CNMFParams']:
        """Compute quality metrics for CNMF-E components.

        Evaluates each component's SNR and spatial correlation, storing
        indices of components that pass the specified thresholds.

        Args:
            estimates: CNMF-E estimates object with extracted components.
            opts_caiman: CaImAn parameters object.
            min_SNR: Minimum signal-to-noise ratio threshold.
            r_values_min: Minimum spatial correlation threshold.

        Returns:
            Tuple of (estimates, opts_caiman) with quality metrics added.
        """
        #This line assumes processing has been performed, and a single memory mapped file exists on your computer under caiman/temp. The filepath should be stored in fnames in opts_caiman
        Yr, dims, T = cm.load_memmap(opts_caiman.get('data', 'fnames')[0])
        images = Yr.T.reshape((T,) + dims, order='F')

        opts_caiman.set('quality', {'min_SNR': min_SNR, 'rval_thr': r_values_min, 'use_cnn': False})
        estimates.evaluate_components(images, opts_caiman)
        return estimates, opts_caiman


    def find_calcium_events_with_deconvolution(
        self, 
        estimates: 'Estimates', 
        opts_caiman: 'CNMFParams', 
        dview: Any, 
        dff_flag: bool = False
    ) -> Dict[int, np.ndarray]:
        """Detect calcium events using deconvolution-based spike inference.

        Uses CaImAn's deconvolution to extract spike trains from calcium
        traces and identifies event indices.

        Args:
            estimates: CNMF-E estimates with calcium traces.
            opts_caiman: CaImAn parameters for deconvolution.
            dview: Distributed view for parallel processing.
            dff_flag: If True, use DF/F traces.

        Returns:
            Dict mapping neuron indices to arrays of event frame indices.
        """
        ca_events_idx = {}
        #ensure deconvolution has not already been performed on estimates
        if not hasattr(estimates, 'S') or estimates.S is None:
            estimates.deconvolve(opts_caiman, dview=dview, dff_flag=dff_flag)

        for k in range(estimates.C.shape[0]):
            spike_train = estimates.S[k]  # Spike train for neuron k
            event_indices = np.where(spike_train > 0)[0]  # Indices of non-zero spikes
            ca_events_idx[k] = event_indices.astype(int)
        return ca_events_idx


    def find_calcium_events_with_derivatives(
        self, 
        estimates: 'Estimates', 
        derivative: str = 'first', 
        event_height: float = 5
    ) -> Dict[int, np.ndarray]:
        """Detect calcium events using derivative-based peak detection.

        Computes the specified derivative of calcium traces and finds
        peaks above the threshold height.

        Args:
            estimates: CNMF-E estimates with calcium traces (C matrix).
            derivative: Order of derivative ('zeroth', 'first', 'second').
            event_height: Minimum peak height threshold.

        Returns:
            Dict mapping neuron indices to arrays of event frame indices.
        """
        print('Finding indices of calcium events...')
        n_components = estimates.C.shape[0]
        neuron_indices = range(n_components)

        if derivative not in ['zeroth', 'first', 'second']:
            raise ValueError("derivative must be 'zeroth', 'first', or 'second'")

        ca_events_idx = {}
        for k in neuron_indices:
            trace = estimates.C[k]
            data = np.array([])
            if derivative == 'zeroth':
                data = trace
            elif derivative == 'first':
                data = np.diff(trace)
            elif derivative == 'second':
                data = np.diff(trace, n=2)
            # Ensure data is at least 1D and has enough points for find_peaks
            if data.size > 0:
                peaks, _ = find_peaks(data, height=event_height)
                ca_events_idx[k] = peaks.astype(int)  # Ensure integer indices
            else:
                ca_events_idx[k] = np.array([], dtype=int)  # Empty array for no peaks
        return ca_events_idx


    @staticmethod
    def compute_miniscope_spectrogram(
        data: np.ndarray, 
        frame_rate: float, 
        window_length: float = 30, 
        window_step: float = 3, 
        freq_lims: List[float] = [0, 15], 
        time_bandwidth: float = 2, 
        plot_spectrogram: bool = True
    ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        """Compute multitaper spectrogram of mean fluorescence signal.

        Uses the multitaper method for robust spectral estimation with
        reduced variance compared to standard periodograms.

        Args:
            data: 1D array of mean fluorescence over time.
            frame_rate: Recording frame rate in Hz.
            window_length: Window length in seconds.
            window_step: Step size between windows in seconds.
            freq_lims: [low, high] frequency range to compute.
            time_bandwidth: Time-bandwidth product (higher = smoother).
            plot_spectrogram: If True, display the spectrogram plot.

        Returns:
            Tuple of (PSD matrix, time points, frequencies, PSD in dB).
        """
        print('Computing spectrogram of average miniscope fluorescence...')
        # Set spectrogram params
        fs = int(frame_rate)
        num_tapers = int(time_bandwidth * 2 - 1)
        window_params = [window_length, window_step]
        minNfft = 0  # No minimum nfft
        detrend_opt = 'constant'  # detrend each window by subtracting the average
        multiprocess = False  # use multiprocessing
        n_jobs = 3  # use 3 cores in multiprocessing
        weighting = 'unity'  # weight each taper at 1
        plot_on = False  # plot spectrogram using multitaper_spectrogram()
        return_fig = False  # do not return plotted spectrogram
        clim_scale = False # do not auto-scale colormap
        verbose = True  # print extra info
        xyflip = False  # do not transpose spect output matrix

        # Compute the multitaper spectrogram and convert the output to decibels
        mt_result = multitaper_spectrogram(data, fs, freq_lims, time_bandwidth, num_tapers, window_params, minNfft, detrend_opt, multiprocess, n_jobs, weighting, plot_on, return_fig, clim_scale, verbose, xyflip)
        PSDSpectMiniscope, tSpect, freqsSpect = mt_result[:3]
        pSpectMiniscope = 10 * np.log10(PSDSpectMiniscope)

        if plot_spectrogram:
            h, ax = misc_functions.spectrogram(tSpect/60, freqsSpect, pSpectMiniscope, xLabel='Time (min)')        

        return PSDSpectMiniscope, tSpect, freqsSpect, pSpectMiniscope


    def compute_miniscope_phase(self, data: np.ndarray) -> np.ndarray:
        """Compute instantaneous phase of fluorescence using Hilbert transform.

        Args:
            data: 1D array of fluorescence signal.

        Returns:
            1D array of instantaneous phase in radians (-pi to pi).
        """
        analytic_signal_miniscope = hilbert(data)
        return np.angle(analytic_signal_miniscope)


    def calculate_component_movie(self, dm: 'MiniscopeDataManager') -> Tuple[cm.movie, cm.movie]:
        """Create movies showing neural activity and background separately.

        Reconstructs the movie as A*C (neural) plus background model.

        Args:
            dm: MiniscopeDataManager with CNMFE results.

        Returns:
            Tuple of (neural_movie, background_movie) as CaImAn movies.
        """
        Yr, dims, T = cm.load_memmap(dm.opts_caiman.get('data', 'fnames')[0])
        neural_activity = dm.CNMFE_obj.estimates.A @ dm.CNMFE_obj.estimates.C  # AC
        neural_movie = cm.movie(neural_activity).reshape(dims + (-1,), order='F').transpose([2, 0, 1])
        background_model = dm.CNMFE_obj.estimates.compute_background(Yr);  # build in function -- explore source code for details
        bg_movie = cm.movie(background_model).reshape(dims + (-1,), order='F').transpose([2, 0, 1])

        return neural_movie, bg_movie

    def calculate_black_component_movie(self, dm: 'MiniscopeDataManager') -> cm.movie:
        """Create a movie with detected neuron regions blacked out.

        Useful for visualizing background activity without neural signals.

        Args:
            dm: MiniscopeDataManager with CNMFE estimates.

        Returns:
            CaImAn movie with neuron ROI pixels set to zero.
        """
        estimates = dm.CNMFE_obj.estimates
        num_frames, movie_height, movie_width = dm.movie.shape
        neuron_mask = np.sum(estimates.A.toarray(), axis=1) > 0  # True where any neuron has non-zero value
        neuron_mask = neuron_mask.reshape(movie_height, movie_width, order='F')  # Reshape to 2D
        movie_without_neurons = dm.movie.copy()
        for frame in range(num_frames):
            movie_without_neurons[frame][neuron_mask] = 0

        print("Calculations complete. Attempting to play movie...", flush=True)
        return movie_without_neurons

__init__(data_manager)

Initialize post-processor with data manager.

Automatically computes movie projections on initialization.

Parameters:

Name Type Description Default
data_manager MiniscopeDataManager

MiniscopeDataManager with movie and CNMFE results.

required
Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
def __init__(self, data_manager: 'MiniscopeDataManager') -> None:
    """Initialize post-processor with data manager.

    Automatically computes movie projections on initialization.

    Args:
        data_manager: MiniscopeDataManager with movie and CNMFE results.
    """
    self.data_manager = data_manager
    self.data_manager.projections = self.compute_projections(self.data_manager.movie)
    self.frame_rate = float(self.data_manager.fr)
    self.dview = self.data_manager.dview

calculate_black_component_movie(dm)

Create a movie with detected neuron regions blacked out.

Useful for visualizing background activity without neural signals.

Parameters:

Name Type Description Default
dm MiniscopeDataManager

MiniscopeDataManager with CNMFE estimates.

required

Returns:

Type Description
movie

CaImAn movie with neuron ROI pixels set to zero.

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
def calculate_black_component_movie(self, dm: 'MiniscopeDataManager') -> cm.movie:
    """Create a movie with detected neuron regions blacked out.

    Useful for visualizing background activity without neural signals.

    Args:
        dm: MiniscopeDataManager with CNMFE estimates.

    Returns:
        CaImAn movie with neuron ROI pixels set to zero.
    """
    estimates = dm.CNMFE_obj.estimates
    num_frames, movie_height, movie_width = dm.movie.shape
    neuron_mask = np.sum(estimates.A.toarray(), axis=1) > 0  # True where any neuron has non-zero value
    neuron_mask = neuron_mask.reshape(movie_height, movie_width, order='F')  # Reshape to 2D
    movie_without_neurons = dm.movie.copy()
    for frame in range(num_frames):
        movie_without_neurons[frame][neuron_mask] = 0

    print("Calculations complete. Attempting to play movie...", flush=True)
    return movie_without_neurons

calculate_component_movie(dm)

Create movies showing neural activity and background separately.

Reconstructs the movie as A*C (neural) plus background model.

Parameters:

Name Type Description Default
dm MiniscopeDataManager

MiniscopeDataManager with CNMFE results.

required

Returns:

Type Description
Tuple[movie, movie]

Tuple of (neural_movie, background_movie) as CaImAn movies.

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
def calculate_component_movie(self, dm: 'MiniscopeDataManager') -> Tuple[cm.movie, cm.movie]:
    """Create movies showing neural activity and background separately.

    Reconstructs the movie as A*C (neural) plus background model.

    Args:
        dm: MiniscopeDataManager with CNMFE results.

    Returns:
        Tuple of (neural_movie, background_movie) as CaImAn movies.
    """
    Yr, dims, T = cm.load_memmap(dm.opts_caiman.get('data', 'fnames')[0])
    neural_activity = dm.CNMFE_obj.estimates.A @ dm.CNMFE_obj.estimates.C  # AC
    neural_movie = cm.movie(neural_activity).reshape(dims + (-1,), order='F').transpose([2, 0, 1])
    background_model = dm.CNMFE_obj.estimates.compute_background(Yr);  # build in function -- explore source code for details
    bg_movie = cm.movie(background_model).reshape(dims + (-1,), order='F').transpose([2, 0, 1])

    return neural_movie, bg_movie

compute_miniscope_phase(data)

Compute instantaneous phase of fluorescence using Hilbert transform.

Parameters:

Name Type Description Default
data ndarray

1D array of fluorescence signal.

required

Returns:

Type Description
ndarray

1D array of instantaneous phase in radians (-pi to pi).

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
def compute_miniscope_phase(self, data: np.ndarray) -> np.ndarray:
    """Compute instantaneous phase of fluorescence using Hilbert transform.

    Args:
        data: 1D array of fluorescence signal.

    Returns:
        1D array of instantaneous phase in radians (-pi to pi).
    """
    analytic_signal_miniscope = hilbert(data)
    return np.angle(analytic_signal_miniscope)

compute_miniscope_spectrogram(data, frame_rate, window_length=30, window_step=3, freq_lims=[0, 15], time_bandwidth=2, plot_spectrogram=True) staticmethod

Compute multitaper spectrogram of mean fluorescence signal.

Uses the multitaper method for robust spectral estimation with reduced variance compared to standard periodograms.

Parameters:

Name Type Description Default
data ndarray

1D array of mean fluorescence over time.

required
frame_rate float

Recording frame rate in Hz.

required
window_length float

Window length in seconds.

30
window_step float

Step size between windows in seconds.

3
freq_lims List[float]

[low, high] frequency range to compute.

[0, 15]
time_bandwidth float

Time-bandwidth product (higher = smoother).

2
plot_spectrogram bool

If True, display the spectrogram plot.

True

Returns:

Type Description
Tuple[ndarray, ndarray, ndarray, ndarray]

Tuple of (PSD matrix, time points, frequencies, PSD in dB).

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
@staticmethod
def compute_miniscope_spectrogram(
    data: np.ndarray, 
    frame_rate: float, 
    window_length: float = 30, 
    window_step: float = 3, 
    freq_lims: List[float] = [0, 15], 
    time_bandwidth: float = 2, 
    plot_spectrogram: bool = True
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Compute multitaper spectrogram of mean fluorescence signal.

    Uses the multitaper method for robust spectral estimation with
    reduced variance compared to standard periodograms.

    Args:
        data: 1D array of mean fluorescence over time.
        frame_rate: Recording frame rate in Hz.
        window_length: Window length in seconds.
        window_step: Step size between windows in seconds.
        freq_lims: [low, high] frequency range to compute.
        time_bandwidth: Time-bandwidth product (higher = smoother).
        plot_spectrogram: If True, display the spectrogram plot.

    Returns:
        Tuple of (PSD matrix, time points, frequencies, PSD in dB).
    """
    print('Computing spectrogram of average miniscope fluorescence...')
    # Set spectrogram params
    fs = int(frame_rate)
    num_tapers = int(time_bandwidth * 2 - 1)
    window_params = [window_length, window_step]
    minNfft = 0  # No minimum nfft
    detrend_opt = 'constant'  # detrend each window by subtracting the average
    multiprocess = False  # use multiprocessing
    n_jobs = 3  # use 3 cores in multiprocessing
    weighting = 'unity'  # weight each taper at 1
    plot_on = False  # plot spectrogram using multitaper_spectrogram()
    return_fig = False  # do not return plotted spectrogram
    clim_scale = False # do not auto-scale colormap
    verbose = True  # print extra info
    xyflip = False  # do not transpose spect output matrix

    # Compute the multitaper spectrogram and convert the output to decibels
    mt_result = multitaper_spectrogram(data, fs, freq_lims, time_bandwidth, num_tapers, window_params, minNfft, detrend_opt, multiprocess, n_jobs, weighting, plot_on, return_fig, clim_scale, verbose, xyflip)
    PSDSpectMiniscope, tSpect, freqsSpect = mt_result[:3]
    pSpectMiniscope = 10 * np.log10(PSDSpectMiniscope)

    if plot_spectrogram:
        h, ax = misc_functions.spectrogram(tSpect/60, freqsSpect, pSpectMiniscope, xLabel='Time (min)')        

    return PSDSpectMiniscope, tSpect, freqsSpect, pSpectMiniscope

compute_projections(movie=None)

Compute spatial and temporal projections of the movie.

Calculates max, min, mean, median, std, range projections and mean fluorescence time series.

Parameters:

Name Type Description Default
movie Optional[movie]

CaImAn movie object to compute projections from.

None

Returns:

Type Description
Projections

Projections object containing all computed projections.

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
def compute_projections(self, movie: Optional[cm.movie] = None) -> Projections:
    """Compute spatial and temporal projections of the movie.

    Calculates max, min, mean, median, std, range projections and
    mean fluorescence time series.

    Args:
        movie: CaImAn movie object to compute projections from.

    Returns:
        Projections object containing all computed projections.
    """
    print("\n\nComputing projections...\n")

    operations = {
        'max': lambda m: np.amax(m, axis=0),
        'std': lambda m: np.std(m, axis=0),
        'min': lambda m: np.amin(m, axis=0),
        'mean': lambda m: np.mean(m, axis=0),
        'median': lambda m: np.median(m, axis=0),
        'time': lambda m: m.mean(axis=(1,2)),
    }

    results = {}
    for name, op in tqdm(operations.items(), desc='Computing Projections'):
        results[name] = op(movie)

    results['range'] = results['max'] - results['min']

    return Projections(
        results['max'],
        results['std'],
        results['min'],
        results['mean'],
        results['median'],
        results['range'],
        results['time']
    )

evaluate_components(estimates, opts_caiman, min_SNR=3, r_values_min=0.85)

Compute quality metrics for CNMF-E components.

Evaluates each component's SNR and spatial correlation, storing indices of components that pass the specified thresholds.

Parameters:

Name Type Description Default
estimates Estimates

CNMF-E estimates object with extracted components.

required
opts_caiman CNMFParams

CaImAn parameters object.

required
min_SNR float

Minimum signal-to-noise ratio threshold.

3
r_values_min float

Minimum spatial correlation threshold.

0.85

Returns:

Type Description
Tuple[Estimates, CNMFParams]

Tuple of (estimates, opts_caiman) with quality metrics added.

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
def evaluate_components(
    self, 
    estimates: 'Estimates', 
    opts_caiman: 'CNMFParams', 
    min_SNR: float = 3, 
    r_values_min: float = 0.85
) -> Tuple['Estimates', 'CNMFParams']:
    """Compute quality metrics for CNMF-E components.

    Evaluates each component's SNR and spatial correlation, storing
    indices of components that pass the specified thresholds.

    Args:
        estimates: CNMF-E estimates object with extracted components.
        opts_caiman: CaImAn parameters object.
        min_SNR: Minimum signal-to-noise ratio threshold.
        r_values_min: Minimum spatial correlation threshold.

    Returns:
        Tuple of (estimates, opts_caiman) with quality metrics added.
    """
    #This line assumes processing has been performed, and a single memory mapped file exists on your computer under caiman/temp. The filepath should be stored in fnames in opts_caiman
    Yr, dims, T = cm.load_memmap(opts_caiman.get('data', 'fnames')[0])
    images = Yr.T.reshape((T,) + dims, order='F')

    opts_caiman.set('quality', {'min_SNR': min_SNR, 'rval_thr': r_values_min, 'use_cnn': False})
    estimates.evaluate_components(images, opts_caiman)
    return estimates, opts_caiman

find_calcium_events_with_deconvolution(estimates, opts_caiman, dview, dff_flag=False)

Detect calcium events using deconvolution-based spike inference.

Uses CaImAn's deconvolution to extract spike trains from calcium traces and identifies event indices.

Parameters:

Name Type Description Default
estimates Estimates

CNMF-E estimates with calcium traces.

required
opts_caiman CNMFParams

CaImAn parameters for deconvolution.

required
dview Any

Distributed view for parallel processing.

required
dff_flag bool

If True, use DF/F traces.

False

Returns:

Type Description
Dict[int, ndarray]

Dict mapping neuron indices to arrays of event frame indices.

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
def find_calcium_events_with_deconvolution(
    self, 
    estimates: 'Estimates', 
    opts_caiman: 'CNMFParams', 
    dview: Any, 
    dff_flag: bool = False
) -> Dict[int, np.ndarray]:
    """Detect calcium events using deconvolution-based spike inference.

    Uses CaImAn's deconvolution to extract spike trains from calcium
    traces and identifies event indices.

    Args:
        estimates: CNMF-E estimates with calcium traces.
        opts_caiman: CaImAn parameters for deconvolution.
        dview: Distributed view for parallel processing.
        dff_flag: If True, use DF/F traces.

    Returns:
        Dict mapping neuron indices to arrays of event frame indices.
    """
    ca_events_idx = {}
    #ensure deconvolution has not already been performed on estimates
    if not hasattr(estimates, 'S') or estimates.S is None:
        estimates.deconvolve(opts_caiman, dview=dview, dff_flag=dff_flag)

    for k in range(estimates.C.shape[0]):
        spike_train = estimates.S[k]  # Spike train for neuron k
        event_indices = np.where(spike_train > 0)[0]  # Indices of non-zero spikes
        ca_events_idx[k] = event_indices.astype(int)
    return ca_events_idx

find_calcium_events_with_derivatives(estimates, derivative='first', event_height=5)

Detect calcium events using derivative-based peak detection.

Computes the specified derivative of calcium traces and finds peaks above the threshold height.

Parameters:

Name Type Description Default
estimates Estimates

CNMF-E estimates with calcium traces (C matrix).

required
derivative str

Order of derivative ('zeroth', 'first', 'second').

'first'
event_height float

Minimum peak height threshold.

5

Returns:

Type Description
Dict[int, ndarray]

Dict mapping neuron indices to arrays of event frame indices.

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
def find_calcium_events_with_derivatives(
    self, 
    estimates: 'Estimates', 
    derivative: str = 'first', 
    event_height: float = 5
) -> Dict[int, np.ndarray]:
    """Detect calcium events using derivative-based peak detection.

    Computes the specified derivative of calcium traces and finds
    peaks above the threshold height.

    Args:
        estimates: CNMF-E estimates with calcium traces (C matrix).
        derivative: Order of derivative ('zeroth', 'first', 'second').
        event_height: Minimum peak height threshold.

    Returns:
        Dict mapping neuron indices to arrays of event frame indices.
    """
    print('Finding indices of calcium events...')
    n_components = estimates.C.shape[0]
    neuron_indices = range(n_components)

    if derivative not in ['zeroth', 'first', 'second']:
        raise ValueError("derivative must be 'zeroth', 'first', or 'second'")

    ca_events_idx = {}
    for k in neuron_indices:
        trace = estimates.C[k]
        data = np.array([])
        if derivative == 'zeroth':
            data = trace
        elif derivative == 'first':
            data = np.diff(trace)
        elif derivative == 'second':
            data = np.diff(trace, n=2)
        # Ensure data is at least 1D and has enough points for find_peaks
        if data.size > 0:
            peaks, _ = find_peaks(data, height=event_height)
            ca_events_idx[k] = peaks.astype(int)  # Ensure integer indices
        else:
            ca_events_idx[k] = np.array([], dtype=int)  # Empty array for no peaks
    return ca_events_idx

postprocess_calcium_movie(remove_components_with_gui=True, find_calcium_events=True, derivative_for_estimates='first', event_height=5, compute_miniscope_phase=True, filter_miniscope_data=True, n=2, cut=[0.1, 1.5], ftype='butter', btype='bandpass', inline=False, compute_miniscope_spectrogram=True, window_length=30, window_step=3, freq_lims=[0, 15], time_bandwidth=2)

Run the complete post-processing pipeline on CNMF-E results.

Performs component curation via GUI, calcium event detection, phase computation, filtering, and spectral analysis.

Parameters:

Name Type Description Default
remove_components_with_gui bool

If True, open interactive GUI for component selection.

True
find_calcium_events bool

If True, detect calcium transient events.

True
derivative_for_estimates str

Derivative order for event detection ('zeroth', 'first', 'second').

'first'
event_height float

Threshold height for peak detection.

5
compute_miniscope_phase bool

If True, compute instantaneous phase via Hilbert.

True
filter_miniscope_data bool

If True, apply bandpass filter to projections.

True
n int

Filter order.

2
cut List[float]

[low, high] cutoff frequencies for bandpass.

[0.1, 1.5]
ftype str

Filter type ('butter', 'fir').

'butter'
btype str

Band type for filter.

'bandpass'
inline bool

If True, replace original data with filtered.

False
compute_miniscope_spectrogram bool

If True, compute multitaper spectrogram.

True
window_length float

Spectrogram window length in seconds.

30
window_step float

Spectrogram step size in seconds.

3
freq_lims List[float]

[low, high] frequency limits for spectrogram.

[0, 15]
time_bandwidth float

Time-bandwidth product for multitaper.

2

Returns:

Type Description
MiniscopeDataManager

Updated MiniscopeDataManager with all post-processing results.

Source code in src/ace_neuro/miniscope/miniscope_postprocessor.py
def postprocess_calcium_movie(
    self, 
    remove_components_with_gui: bool = True,  
    find_calcium_events: bool = True,
    derivative_for_estimates: str = 'first', 
    event_height: float = 5, 
    compute_miniscope_phase: bool = True, 
    filter_miniscope_data: bool = True,
    n: int = 2, 
    cut: List[float] = [0.1, 1.5], 
    ftype: str = 'butter', 
    btype: str = 'bandpass', 
    inline: bool = False,
    compute_miniscope_spectrogram: bool = True,
    window_length: float = 30, 
    window_step: float = 3, 
    freq_lims: List[float] = [0, 15], 
    time_bandwidth: float = 2
) -> 'MiniscopeDataManager':
    """Run the complete post-processing pipeline on CNMF-E results.

    Performs component curation via GUI, calcium event detection, phase
    computation, filtering, and spectral analysis.

    Args:
        remove_components_with_gui: If True, open interactive GUI for component selection.
        find_calcium_events: If True, detect calcium transient events.
        derivative_for_estimates: Derivative order for event detection ('zeroth', 'first', 'second').
        event_height: Threshold height for peak detection.
        compute_miniscope_phase: If True, compute instantaneous phase via Hilbert.
        filter_miniscope_data: If True, apply bandpass filter to projections.
        n: Filter order.
        cut: [low, high] cutoff frequencies for bandpass.
        ftype: Filter type ('butter', 'fir').
        btype: Band type for filter.
        inline: If True, replace original data with filtered.
        compute_miniscope_spectrogram: If True, compute multitaper spectrogram.
        window_length: Spectrogram window length in seconds.
        window_step: Spectrogram step size in seconds.
        freq_lims: [low, high] frequency limits for spectrogram.
        time_bandwidth: Time-bandwidth product for multitaper.

    Returns:
        Updated MiniscopeDataManager with all post-processing results.
    """

    if remove_components_with_gui:
        if self.data_manager.CNMFE_obj is not None and self.data_manager.CNMFE_obj.estimates.A is not None and self.data_manager.CNMFE_obj.estimates.A.shape[0] > 0:
            if hasattr(self.data_manager, 'diag_logger') and self.data_manager.diag_logger is not None: self.data_manager.diag_logger.pause_timer()
            self.data_manager.CNMFE_obj.estimates.plot_contours()
            self.data_manager.CNMFE_obj.estimates = component_gui(self.data_manager.movie, self.data_manager.CNMFE_obj.estimates, self.data_manager.projections)
            if hasattr(self.data_manager, 'diag_logger') and self.data_manager.diag_logger is not None: self.data_manager.diag_logger.resume_timer()
        else:
            print("No components found or CNMF-E object is None. Skipping component GUI.")

    if find_calcium_events:
        if self.data_manager.CNMFE_obj is not None and self.data_manager.CNMFE_obj.estimates.C is not None:
            self.data_manager.ca_events_idx = self.find_calcium_events_with_derivatives(self.data_manager.CNMFE_obj.estimates, derivative_for_estimates, event_height)
        else:
            print("WARNING: No CNMF-E components found (estimates.C is None). Skipping calcium event detection.")
            self.data_manager.ca_events_idx = {}

    if compute_miniscope_spectrogram:
        data = self.data_manager.projections.time
        PSDSpectMiniscope, tSpect, freqsSpect, pSpectMiniscope = self.compute_miniscope_spectrogram(data, frame_rate=self.frame_rate, window_length=window_length, window_step=window_step, freq_lims=freq_lims, time_bandwidth=time_bandwidth)
        h, ax = misc_functions.spectrogram(tSpect/60, freqsSpect, pSpectMiniscope, xLabel='Time (min)')
        self.data_manager.PSD_spect, self.data_manager.t_spect, self.data_manager.freqs_spect, self.data_manager.p_spect = PSDSpectMiniscope, tSpect, freqsSpect, pSpectMiniscope

    if compute_miniscope_phase:
        self.data_manager.miniscope_phases = self.compute_miniscope_phase(self.data_manager.projections.time)

    if filter_miniscope_data:
        filter_object = FilterMiniscopeData(self.data_manager.projections, self.frame_rate, n=n, cut=cut, ftype=ftype, btype=btype)
        filter_object.filter_miniscope_data
        self.data_manager.filter_object = filter_object

        if inline == True:
            self.data_manager.projections.time = filter_object.filtered_data

    return self.data_manager