feat(cadecon): noise-constrained sparsity for low-SNR deconvolution#168
Merged
Conversation
Add an optional, knob-free sparsity to the InDeCa spike inference that
suppresses noise being fit as spurious activity. Instead of picking the
binarization threshold that maximizes fit (which overfits into the noise),
pick the sparsest spike support whose reconstruction residual still reaches
the data-derived noise floor ("don't spend spikes explaining what's within
the noise"). The effect concentrates at low SNR and is neutral at high SNR.
Core (crates/solver):
- threshold.rs: Selection::{MaxPve,NoiseFloor}; threshold_search_opts +
select_noise_floor_threshold pick the highest (sparsest) threshold whose
residual, evaluated at original-rate grid positions, stays within sigma^2*T.
- indeca.rs: SolveOptions{noise_constrained, collapse_runs}; solve_trace_opts.
Noise sigma is LP-robust: raw high-frequency-PSD estimate (high_band_sigma)
rescaled by the empirically measured noise gain of the upsample+HP/LP chain
(estimate_grid_noise_sigma), so it tracks the noise that survives the filters.
- upsample.rs: collapse_runs (event-preserving reduction; not wired to UI).
- js_indeca.rs / py_api.rs: expose the flag through the WASM + PyO3 bindings.
App (apps/cadecon):
- Thread noise_constrained through worker -> pool -> iteration-manager, read
from a new algorithm-store signal. Default ON.
- Teaching/impact overlay (opt-in, default OFF): when enabled, each trace is
solved with BOTH sparsity settings per iteration and the opposite-setting
counts are stored; the Trace Inspector overlays them and shows both spike
counts. Precomputed in the run (no main-thread solving) so browsing stays
smooth. Lives under a new "Display Options" panel (it is a visual-inspection
aid, not an algorithm knob) and doubles run time, documented in-UI and in
the "Reading Convergence & Results" tutorial.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDPugMWynodvqCBPH4p8ab
29f5f67 to
77546cd
Compare
…nto Python, backfill changelog Remove the last collapse-runs reference (stale doc comment), expose the noise_constrained option through the calab.solve_trace Python binding, and document it in the CaDecon guide. Restructure docs/CHANGELOG.md: split the consolidated post-2.0.6 blob into the real minor releases (2.1.0-2.4.0), add 2.5.0 (PRs #153-#167), and scope 2.6.0 to PR #168. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…; review cleanups Council review caught a correctness bug: the outer scale-iteration loop always selected the best iterate by max PVE, even under noise-constrained selection — re-selecting the densest-fitting iteration after the inner threshold search had deliberately stopped at the noise floor, partially re-laundering the sparsity. Under NoiseFloor the outer loop now selects the best-calibrated prescale (min scale-convergence error), the loop's own fixed point; MaxPve is unchanged (historical output byte-identical). Also folds in the prior review cleanups: - unify grid_sse_at_threshold into evaluate_threshold via a stride param - top-down early-exit noise-floor scan (identical result, fewer evaluations) - Rust tests: budget-ordering invariant, infeasible fallback, high_band_sigma recovery, end-to-end noisy-trace recovery; Python noisy-trace test - doc rewording (drop "experiment"/"count-debiasing"); corrected viz-store overlay comment; consolidated TraceInspector label helpers - UI: clarify the toggle describes a stopping rule + SNR≈1 trim caveat Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional, knob-free sparsity to the InDeCa spike inference that suppresses noise being fit as spurious activity. Rather than picking the binarization threshold that maximizes fit (which overfits into the noise floor), it picks the sparsest spike support whose reconstruction residual still reaches the data-derived noise floor — "don't spend spikes explaining anything that's within the noise." The effect self-scales with SNR: strong at low SNR, negligible at high SNR.
Default ON in the CaDecon app (library default is off /
MaxPve, so existing callers are byte-identical). There is no tuning parameter — σ is measured from the data.How it works
σ²·n_grid(the Morozov discrepancy principle). Falls back to the min-residual threshold if none is feasible.MaxPve(unchanged) but by best prescale calibration underNoiseFloor, so the outer loop doesn't re-select the densest-fitting iteration and undo the sparsity the inner search just imposed.Teaching / impact overlay (opt-in, default OFF)
To see the sparsity's effect, an opt-in overlay solves each trace with both settings per iteration and stores the opposite-setting counts; the Trace Inspector overlays them and shows both spike counts. It's precomputed during the run (no main-thread solving) so browsing stays smooth, roughly doubles run time, and lives under a Display Options panel — a visual-inspection aid, not an algorithm knob. Documented in-UI and in the "Reading Convergence & Results" tutorial.
Also in this PR
collapse_runslever — it collapsed genuine dense spike trains and was shown harmful on bursty data; only the noise-constrained path ships.calab.solve_trace(..., noise_constrained=False)exposes the option through PyO3; documented in the CaDecon guide.flex-shrink, so a short viewport (or large text) compressed and overlapped them instead of scrolling. Panels are now fixed-size and the sidebar scrolls.Testing
cargo test -p calab-solver— 135 passed. New tests cover the noise-floor budget-ordering invariant (larger budget → sparser support), the infeasible-fallback path,high_band_sigmarecovering an injected white-noise σ, and end-to-end recovery on a noisy trace.pytest— 18 passed (adds a noisy-tracenoise_constrainedintegration test).tscclean; CI green (rust, python 3.11/3.12/3.13, format, supabase).🤖 Generated with Claude Code