Sum-to-1 Constrained Ring Background¶
Opt-in extension to the standard CNMF-E ring model. Enabled with
CNMFeParams(ring_constrain_sum=True). Off by default — set explicitly to True for recordings with global brightness events.See also: algorithm math §6 (ring background), API reference for
compute_W.
Problem¶
The standard CNMF-E ring model (Zhou et al. 2018) predicts the background at pixel \(i\) from a ridge-regularised LS fit over its ring of neighbours:
with compute_W solving the unconstrained problem
This works well for local spatial structure (vasculature halos, slow gradients) but fails on spatially-uniform brightness changes — LED flicker, sensor gain shifts, uniform photobleaching, motion-coupled illumination.
If every pixel drops by a constant \(D\) at frame \(t\), the residual at pixel \(i\) is
With ridge regularisation, \(\sum_j W_{ij} < 1\), so a fraction of \(D\) leaks through and is re-projected onto every extracted component's trace.
Observed symptoms¶
tmp/output.png (real recording, 2000 frames) showed both modes:
- Synchronous sharp dips at ~650, 950, 1100, 1450, 1700, 1900 across every extracted trace — global brightness events leaking through.
- A large slow-drifting "neuron" (orange trace, ~2.0 → ~1.0) — a ghost component parked on residual photobleaching that the ring couldn't cancel, also collecting the leaked dips.
Fix¶
Add the equality constraint \(\sum_j W_{ij} = 1\) per row. Any spatially-uniform mode then cancels exactly:
Neural signal is sparse and non-global, so it is unaffected.
Solver¶
Solved via Lagrangian — one extra RHS in the already-factored ridge gram:
# Gram (unchanged): M = B B^T + lambda * trace(B B^T) * I
# Solve two RHSs at once
sols = solve(M, column_stack([B @ y, ones(n_ring)])) # (n_ring, 2)
w_unc, w_corr = sols[:, 0], sols[:, 1]
# Lagrange correction to satisfy 1^T w = 1
mu = (1 - w_unc.sum()) / w_corr.sum()
w = w_unc + mu * w_corr
Cost: one extra RHS in a per-pixel solve that was already happening — negligible.
Code locations¶
| File | What |
|---|---|
minicnmfe/background.py:30-65 |
_ring_pixel_batch — CPU serial/joblib path |
minicnmfe/background.py:67-122 |
_ring_pixel_batch_slab — zarr-streaming path |
minicnmfe/background.py:154-242 |
_compute_W_gpu — batched CuPy path |
minicnmfe/background.py:341-491 |
compute_W — entry point + dispatcher |
minicnmfe/pipeline.py:50-130 |
CNMFeParams.ring_constrain_sum (new field) |
minicnmfe/pipeline.py:656, 760 |
Two compute_W call sites in CNMFe.fit() |
tests/test_background.py |
TestRingConstrainSum — 4 tests |
Verification¶
Four targeted tests in tests/test_background.py::TestRingConstrainSum:
test_row_sums_are_one— every non-empty row of \(W\) sums to \(1 \pm 10^{-4}\).test_unconstrained_rows_do_not_sum_to_one— confirms the unconstrained ridge solution shrinks row sums visibly below 1 (the failure mode this fix targets).test_global_pulse_cancels— injects a uniform \(-5.0\) brightness drop at one frame, fits \(W\) on the clean movie, applies to both versions. Constrained leak is at the float-precision floor (\(< 10^{-4}\)); unconstrained leak is ~\(2.4\) RMS. Ratio > \(10^4 \times\) better.test_default_is_unconstrained— callingcompute_Wwithout the kwarg produces an identical matrix toconstrain_sum=False; confirms the standard CNMF-E behaviour is preserved when the flag is off.
All four pass. All 115 pre-existing background/pipeline tests still pass.
When to enable¶
Set ring_constrain_sum=True when the recording has:
- Visible LED flicker or sensor gain steps (synchronous brightness changes across the frame).
- Strong global photobleaching.
- Motion-coupled illumination changes the rigid MC can't compensate for.
Leave the default (False) for:
- Strict reproduction of the published CNMF-E algorithm.
- Recordings where the unconstrained ring already works (most lab-quality data without visible global artefacts).
Limitations / what this does NOT fix¶
The constraint only cancels modes that are spatially uniform. If the global
event has spatial structure (e.g. one corner dims more than another, vignetting-
coupled bleach), partial leakage remains. The next-step extension would be a
per-frame scalar background term \(f(t)\) (CaImAn-style nb >= 1) layered on top
of the ring — deferred until evidence shows the constrained ring is insufficient
on the recording of interest.
Standard CNMF-E reminder¶
This is a non-standard extension. The original Zhou et al. 2018 algorithm uses
unconstrained ridge LS. CNMFeParams tags every non-standard knob with
[NON-STANDARD] in its inline comment so a reader can audit which defaults differ
from the published algorithm. See the CNMFeParams docstring for the full
convention.