CNMFe — Architecture & File Map¶
See API reference for function signatures. See usage guide for how to run the pipeline.
Repository Layout¶
miniCNMFe/
├── minicnmfe/ # Main package
│ ├── __init__.py # Public re-exports: CNMFe, CNMFeParams, load_movie
│ ├── _utils.py # Shared low-level helpers (no algorithm logic)
│ ├── io.py # File I/O: AVI/MP4 -> zarr, open/save zarr
│ ├── motion_correction.py # Rigid motion correction (FFT phase correlation)
│ ├── preprocess.py # Noise estimation, center-surround PSF, CORR/PNR
│ ├── background.py # Ring-model background (compute_W, subtract_background)
│ ├── initialization.py # Greedy CORR-PNR seed detection and extraction
│ ├── spatial.py # Spatial footprint update (elastic-net coordinate descent per pixel)
│ ├── temporal.py # Temporal update + OASIS AR deconvolution
│ ├── merging.py # Component merging (overlap + correlation)
│ ├── concat_avis_to_zarr.py # Concatenate 0.avi...N.avi into one zarr (importable + `python -m`)
│ └── pipeline.py # CNMFeParams + CNMFe.fit() orchestrator
├── tests/
│ ├── conftest.py # make_synthetic_movie() fixture; supports motion_max_shift
│ ├── miniscope_simulator.py # make_miniscope_movie() realistic simulator; supports motion_max_shift
│ ├── test_io.py
│ ├── test_motion_correction.py
│ ├── test_preprocess.py
│ ├── test_background.py
│ ├── test_initialization.py
│ ├── test_spatial.py
│ ├── test_temporal.py
│ ├── test_pipeline.py
│ └── test_multiprocessing.py # n_jobs parallelism tests
├── docs/ # This documentation (guides, concepts, api, tuning)
├── demo_movies/ # Generated AVI + zarr + meta files (created by scripts)
├── generate_demo_movies.py # Generate demo_movies/*.avi with ground-truth NPZ sidecars
├── convert_to_zarr.py # Batch-convert demo_movies/*.avi -> *.zarr
├── full_pipeline.py # CLI: load zarr, run full pipeline, save results to disk
├── demo_notebooks/ # Tutorial notebooks (01 load+MC, 02 tune, 03 extract, 04 advanced)
└── pyproject.toml
Module Dependency Graph¶
graph TD
pipeline --> motion_correction
pipeline --> preprocess
pipeline --> initialization
pipeline --> background
pipeline --> spatial
pipeline --> temporal
pipeline --> merging
pipeline --> _utils
initialization --> preprocess
initialization --> temporal
spatial --> _utils
background --> _utils
motion_correction --> _utils
preprocess --> _utils
Note:
_utils.pyis the only module with no internal imports. All other modules may import from_utils. Circular imports are not possible in this layout.
Data Flow¶
Types in play¶
| Variable | dtype | Shape | Where created |
|---|---|---|---|
movie / movie_arr |
float32 | (T, H, W) |
input |
Y_flat |
float32 | (H·W, T) |
_utils.make_2d |
sn |
float32 | (H, W) |
preprocess.estimate_noise |
sn_flat |
float32 | (H·W,) |
.ravel() of sn |
cn, pnr |
float32 | (H, W) |
preprocess.correlation_pnr |
A |
float32 sparse csc | (H·W, K) |
initialization.greedy_corr_pnr |
C |
float32 | (K, T) |
same |
C_raw |
float32 | (K, T) |
same |
g_per_k |
list of (p,) arrays |
length K |
pipeline.fit (pooled estimate from C_raw.ravel(), persisted) |
sn_per_k |
float32 | (K,) |
pipeline.fit |
W_mat |
float32 sparse csr | (H·W, H·W) |
background.compute_W |
b0 |
float32 | (H·W,) |
same |
Y_bg |
float32 | (H·W, T) |
background.subtract_background |
S |
float32 | (K, T) |
temporal.update_temporal |
YrA |
float32 | (K, T) |
pipeline.fit (residual at each footprint after final BCD; C + YrA = noisy projection) |
shifts |
float32 | (T, 2) |
motion_correction.motion_correction_rigid |
Pipeline step by step¶
movie_arr (T, H, W)
│
▼ motion_correction_rigid() (or fit_mc / fused fit_mc_from_avis)
movie_arr (T, H, W) + shifts (T, 2)
│
├──► estimate_noise() ──────────────────► sn (H, W)
│
├──► correlation_pnr() ─────────────────► cn (H, W), pnr (H, W)
│
├──► [if decay_time_ms + frame_rate_hz set]
│ g_target = exp(-1 / (fps · τ_ms / 1000))
│ (Bayesian prior threaded into every estimate_ar_params call below)
│
├──► greedy_corr_pnr(g_prior=g_target) ─► A (H·W, K), C (K, T), C_raw
│ (per-component estimate_ar_params inside greedy init also
│ receives the prior; OASIS deconvolution at each seed runs
│ against the prior-shrunk g)
│
├──► make_2d() ─────────────────────────► Y_flat (H·W, T)
│
├──► estimate_ar_params( ► g_per_k (cached), sn_per_k
│ C_raw.ravel(),
│ g_prior=g_target,
│ g_prior_weight=p.g_prior_weight,
│ )
│ g_post = (1 - w) · g_yw + w · g_target (fudge_factor bypassed
│ on prior path; legacy multiplicative shrinkage when prior is None)
│
├──► compute_W() ───────────────────────► W_mat (H·W, H·W), b0 (H·W)
│
└── for n_iter_main iterations:
│
├──► [iter 0 only]
│ merge_components() ────────► A, C, members_per_group
│ _cache_after_merge() ─► g_per_k, sn_per_k (inherit)
│
├──► subtract_background() ─────► Y_bg (H·W, T)
├──► update_spatial() ──────────► A (H·W, K)
├──► [prune dead components] ─► A, C, g_per_k, sn_per_k
├──► update_temporal(g_cached=…)─► C, S, g_per_k, sn_per_k (cache reused, no drift)
├──► merge_components() ────────► A, C, members_per_group
├──► _cache_after_merge() ─► g_per_k, sn_per_k (inherit from members[0])
├──► [if merged] update_temporal()
└──► compute_W() ────────────────► W_mat, b0 (refresh)
│
├──► update_temporal() (final pass) ────► C, S, g_per_k, sn_per_k
└──► [compute residual projection] ────► YrA (K, T) — model.C + model.YrA = noisy projection
Parallelism Model¶
Each step that supports n_jobs uses joblib.Parallel with the loky backend (process-based, Windows-compatible). Worker functions are defined at module level to be picklable.
Parallelism is axis-aligned — partitioning along frames, flattened pixels, or neurons depending on the step. The full FOV is always processed as one piece; there is no spatial tiling.
| Step | Axis | Worker | Unit of work |
|---|---|---|---|
correlation_pnr (PSF conv) |
frames (T) | scipy.ndimage.convolve |
one frame |
greedy_corr_pnr (PSF conv) |
frames (T) | scipy.ndimage.convolve |
one frame |
motion_correction_rigid |
frames (T) | _filter_estimate_apply (per batch via _process_batch) |
one frame |
compute_W (ring background) |
pixels (H·W) | _ring_pixel_batch |
500 pixels per batch |
update_spatial |
pixels (H·W) | _spatial_pixel_batch |
256 pixels per batch |
update_temporal (OASIS) |
neurons (K) | _deconvolve_with |
one component |
Warning: On Windows,
n_jobs != 1usesspawn(no fork). Avoid large global state; prefer passing arguments explicitly. The greedy seed loop ingreedy_corr_pnris itself sequential — only the per-frame PSF convolution that feeds it is parallel. By default (init_patches=True) the FOV is split into overlapping patches run in parallel processes (greedy_corr_pnr_patched), but each patch's seed loop is still sequential. OASIS is sequential along T per component (cannot be parallelised within a trace), only across K.
Why axis-aligned and not patch-based (vs CaImAn)¶
CaImAn's rf parallelism splits the FOV into overlapping spatial tiles, runs full CNMF per tile in parallel, then stitches. That scales to FOVs that don't fit in RAM, at the cost of edge artefacts and a stitching/merge step across patch boundaries.
minicnmfe/ parallelises along T / pixels / K instead. Every step sees the whole FOV, so there is no boundary stitching and no patch-boundary duplicate components — but the in-memory (T, H, W) movie is the hard ceiling. Motion correction already streams from zarr; the BCD stages (update_spatial, update_temporal, compute_W) currently require the full movie in RAM. For very large recordings, adding patch-based parallelism or zarr-streamed BCD would be the natural extensions.
Key Design Decisions¶
Zarr-native I/O¶
All input formats are converted to zarr (time-chunked) before processing. The pipeline reads frames in batches without loading the full movie into RAM.
Flat pixel representation¶
After initialisation, the movie is stored as (H·W, T) — pixels as rows, time as columns. This makes the matrix products \(\mathbf{A}\mathbf{C}\) and \(\mathbf{A}^\top\mathbf{Y}\) straightforward dense-sparse multiplications.
No CaImAn imports¶
All math is reimplemented from scratch using numpy/scipy/skimage/sklearn. CaImAn source is referenced for algorithm design only.
Module-level worker functions¶
Functions dispatched by joblib.Parallel (e.g. _filter_estimate_apply, _deconvolve_with) are always defined at the top level of their module, not as lambdas or nested functions. This is required for pickling on Windows (spawn process start method). _deconvolve_with replaced the older _deconvolve_one and takes pre-computed g/sn so it does not re-estimate AR params per call.
Bayesian-prior path for AR coefficient g¶
estimate_ar_params has two shrinkage paths. The legacy path multiplies the Yule-Walker estimate by fudge_factor (default 0.96) — a unitless prior toward zero. The prior path takes g_prior = exp(-1 / (fps · τ_ms / 1000)) derived from CNMFeParams.decay_time_ms + frame_rate_hz and shrinks toward it: g = (1 - g_prior_weight) · g_yw + g_prior_weight · g_prior. fudge_factor is bypassed on the prior path.
g_target is computed once in CNMFe.fit and threaded into every estimator: pipeline-init pooled estimate, per-component init estimate (greedy extract_spatial_temporal), and the update_temporal fallback when the cache is empty. With this, the same shrinkage target governs the AR coefficient end-to-end across init → BCD → final pass.