From d6dba2a4e9bbfda0602775785140b821c97bb58d Mon Sep 17 00:00:00 2001 From: daharoni Date: Fri, 6 Mar 2026 15:04:03 -0800 Subject: [PATCH 1/4] feat(catune): replace rise/decay sliders with log-scale DualRangeSlider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both time constant thumbs now share a single logarithmic track (0.001–3.0s), making their relationship visually obvious and giving the rise thumb adequate resolution. Includes true-value markers, onCommit, and tutorial data attrs. Co-Authored-By: Claude Opus 4.6 --- .../components/controls/DualRangeSlider.tsx | 180 ++++++++++++++++++ .../components/controls/ParameterPanel.tsx | 36 ++-- apps/catune/src/styles/controls.css | 117 ++++++++++++ 3 files changed, 314 insertions(+), 19 deletions(-) create mode 100644 apps/catune/src/components/controls/DualRangeSlider.tsx diff --git a/apps/catune/src/components/controls/DualRangeSlider.tsx b/apps/catune/src/components/controls/DualRangeSlider.tsx new file mode 100644 index 0000000..9a6ef9b --- /dev/null +++ b/apps/catune/src/components/controls/DualRangeSlider.tsx @@ -0,0 +1,180 @@ +// Dual-thumb range slider for rise/decay time constants on a shared log-scale track. +// Both thumbs share one time axis so their positions are directly comparable. + +import { Show } from 'solid-js'; +import type { JSX, Accessor } from 'solid-js'; +import { notifyTutorialAction, isTutorialActive } from '@calab/tutorials'; + +interface DualRangeSliderProps { + label: string; + lowLabel: string; + highLabel: string; + lowValue: Accessor; + highValue: Accessor; + setLowValue: (v: number) => void; + setHighValue: (v: number) => void; + /** Overall track range (min of the shared log axis). */ + min: number; + /** Overall track range (max of the shared log axis). */ + max: number; + /** Clamp bounds for the low thumb. */ + lowMin: number; + lowMax: number; + /** Clamp bounds for the high thumb. */ + highMin: number; + highMax: number; + format?: (v: number) => string; + unit?: string; + lowTrueValue?: number; + highTrueValue?: number; + 'data-tutorial-low'?: string; + 'data-tutorial-high'?: string; + onCommit?: () => void; +} + +const toLog = (v: number): number => Math.log(v); +const fromLog = (p: number): number => Math.exp(p); + +function parseInput(e: Event): number | null { + const raw = parseFloat((e.target as HTMLInputElement).value); + return isNaN(raw) ? null : raw; +} + +export function DualRangeSlider(props: DualRangeSliderProps): JSX.Element { + const fmt = (v: number) => (props.format ? props.format(v) : v.toString()); + + const sliderMin = () => toLog(props.min); + const sliderMax = () => toLog(props.max); + const sliderRange = () => sliderMax() - sliderMin(); + + const toPct = (v: number) => ((toLog(v) - sliderMin()) / sliderRange()) * 100; + + const lowPct = () => toPct(props.lowValue()); + const highPct = () => toPct(props.highValue()); + + // --- Range input handlers (log-space) --- + + function handleLowRange(e: Event): void { + const logVal = parseInput(e); + if (logVal === null) return; + const val = fromLog(logVal); + const clamped = Math.max(props.lowMin, Math.min(props.lowMax, val)); + props.setLowValue(clamped); + } + + function handleHighRange(e: Event): void { + const logVal = parseInput(e); + if (logVal === null) return; + const val = fromLog(logVal); + const clamped = Math.max(props.highMin, Math.min(props.highMax, val)); + props.setHighValue(clamped); + } + + function handleCommit(): void { + props.onCommit?.(); + if (isTutorialActive()) notifyTutorialAction(); + } + + // --- Numeric input handlers (display-space, ms) --- + + function handleLowNumeric(e: Event): void { + const raw = parseInput(e); + if (raw === null) return; + // Numeric input shows ms, convert back to seconds + const seconds = raw / 1000; + const clamped = Math.max(props.lowMin, Math.min(props.lowMax, seconds)); + props.setLowValue(clamped); + } + + function handleHighNumeric(e: Event): void { + const raw = parseInput(e); + if (raw === null) return; + const seconds = raw / 1000; + const clamped = Math.max(props.highMin, Math.min(props.highMax, seconds)); + props.setHighValue(clamped); + } + + const STEP_LOG = 0.01; + + return ( +
+
+ {props.label} +
+
+
+ +
+
+ +
+
+
+
+ + + +
+ + +
+ +
+
+ ); +} diff --git a/apps/catune/src/components/controls/ParameterPanel.tsx b/apps/catune/src/components/controls/ParameterPanel.tsx index 28ce1f2..5e68f4b 100644 --- a/apps/catune/src/components/controls/ParameterPanel.tsx +++ b/apps/catune/src/components/controls/ParameterPanel.tsx @@ -16,6 +16,7 @@ import { notifyTutorialAction } from '@calab/tutorials'; import { isDemo, demoPreset, groundTruthVisible } from '../../lib/data-store.ts'; import { PARAM_RANGES } from '@calab/core'; import { ParameterSlider } from './ParameterSlider.tsx'; +import { DualRangeSlider } from './DualRangeSlider.tsx'; import '../../styles/controls.css'; export function ParameterPanel() { @@ -43,29 +44,26 @@ export function ParameterPanel() { return (
- (v * 1000).toFixed(1)} - unit="ms" - data-tutorial="slider-rise" - trueValue={trueRise()} - /> - (v * 1000).toFixed(1)} unit="ms" - data-tutorial="slider-decay" - trueValue={trueDecay()} + data-tutorial-low="slider-rise" + data-tutorial-high="slider-decay" + lowTrueValue={trueRise()} + highTrueValue={trueDecay()} /> Date: Fri, 6 Mar 2026 15:12:23 -0800 Subject: [PATCH 2/4] fix(ui): pack card grid rows to top to eliminate vertical gaps Add align-content: start so grid rows don't stretch to fill the container, which caused large gaps between card rows. Co-Authored-By: Claude Opus 4.6 --- packages/ui/src/styles/layout.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/styles/layout.css b/packages/ui/src/styles/layout.css index 6b64d94..5b1b1ad 100644 --- a/packages/ui/src/styles/layout.css +++ b/packages/ui/src/styles/layout.css @@ -264,6 +264,7 @@ html.dashboard-mode #root { --grid-cols: 2; display: grid; grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr)); + align-content: start; gap: var(--panel-gap); overflow-y: auto; overflow-x: hidden; From 87f5474eca53f1d4b0d3505cb9eb921726b7c668 Mon Sep 17 00:00:00 2001 From: daharoni Date: Fri, 6 Mar 2026 16:17:08 -0800 Subject: [PATCH 3/4] docs(catune): update tutorials to document automatic baseline handling Tutorials previously omitted or contradicted CaTune's rolling-percentile baseline subtraction. Update theory (steps 2, 9) and advanced (steps 2, 5) tutorials to accurately describe the three-layer baseline handling: rolling-percentile subtraction, bandpass filter, and solver estimate. Co-Authored-By: Claude Opus 4.6 --- .../catune/src/lib/tutorial/content/03-advanced.ts | 4 ++-- apps/catune/src/lib/tutorial/content/05-theory.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/catune/src/lib/tutorial/content/03-advanced.ts b/apps/catune/src/lib/tutorial/content/03-advanced.ts index 40be94b..f0ad444 100644 --- a/apps/catune/src/lib/tutorial/content/03-advanced.ts +++ b/apps/catune/src/lib/tutorial/content/03-advanced.ts @@ -23,7 +23,7 @@ export const advancedTutorial: Tutorial = { element: '[data-tutorial="card-grid"]', title: 'Residual Pattern Analysis', description: - 'The red residual trace reveals model mismatches. Systematic positive bumps after peaks: decay too short. Negative dips before peaks: rise too long. Low-frequency waves: baseline drift (not a parameter issue). Residuals should resemble noise. Near-zero residuals = overfitting. Visible transient shapes = underfitting.', + 'The red residual trace reveals model mismatches. Systematic positive bumps after peaks: decay too short. Negative dips before peaks: rise too long. Low-frequency waves: residual baseline drift (the automatic baseline subtraction handles most drift, but very slow trends may remain). Residuals should resemble noise. Near-zero residuals = overfitting. Visible transient shapes = underfitting.', side: 'bottom', }, // Step 3: Parameter coupling @@ -47,7 +47,7 @@ export const advancedTutorial: Tutorial = { element: '[data-tutorial="card-grid"]', title: 'Artifacts & Challenging Signals', description: - 'Common artifacts that affect fitting: Motion artifacts: sharp, symmetric deflections (not calcium-shaped). Photobleaching: slow downward baseline trend. Neuropil contamination: broad, slow signals mixed with sharp events. These cannot be fixed by parameter tuning \u2014 they require preprocessing.

' + + 'Common artifacts that affect fitting: Motion artifacts: sharp, symmetric deflections (not calcium-shaped). Photobleaching: slow downward baseline trend (largely handled by the automatic rolling-percentile baseline subtraction, but extreme cases may still affect results). Neuropil contamination: broad, slow signals mixed with sharp events. Motion artifacts cannot be fixed by parameter tuning \u2014 they require preprocessing. Photobleaching and neuropil contamination are partially addressed by CaTune\u2019s automatic baseline handling.

' + 'When neurons fire rapidly, calcium events overlap. The model handles this via superposition, but dense firing can make individual events hard to resolve. Under big fluorescence events, try increasing decay time to reduce dense deconvolved activity \u2014 increase as much as possible without making the fit too poor.', side: 'bottom', popoverClass: 'driver-popover--wide', diff --git a/apps/catune/src/lib/tutorial/content/05-theory.ts b/apps/catune/src/lib/tutorial/content/05-theory.ts index 4a991f6..b872422 100644 --- a/apps/catune/src/lib/tutorial/content/05-theory.ts +++ b/apps/catune/src/lib/tutorial/content/05-theory.ts @@ -39,7 +39,8 @@ export const theoryTutorial: Tutorial = { '3. Slow baseline drift — out-of-focus neuropil, diffuse calcium dynamics, photobleaching

' + 'Mathematically:
' + 'F(t) = (s \u2217 k)(t) + b(t) + \u03B5(t)

' + - 'where s is neural activity, k is the calcium kernel, b is baseline drift, and \u03B5 is noise. Deconvolution aims to recover s from F.', + 'where s is neural activity, k is the calcium kernel, b is baseline drift, and \u03B5 is noise. Deconvolution aims to recover s from F.

' + + 'CaTune automatically handles b(t) through rolling-percentile baseline subtraction before deconvolution, so you generally don\u2019t need to preprocess for baseline drift.', }, // Step 3: The calcium kernel { @@ -104,12 +105,13 @@ export const theoryTutorial: Tutorial = { }, // Step 9: The role of noise filtering { - title: 'The Role of Noise Filtering', + title: 'Baseline & Noise Handling', description: - 'High-frequency noise creates spurious transients in the deconvolved trace. The Noise Filter applies a bandpass derived from your kernel parameters:

' + - '\u2022 The high-pass removes slow drift (baseline)
' + - '\u2022 The low-pass removes noise above what your calcium dynamics can produce

' + - "Filtering is conservative — it won't change transient shapes — but it significantly cleans up the deconvolution.", + 'CaTune handles baseline in three layers:

' + + '1. Rolling-percentile baseline subtraction \u2014 always active. Before every solve, a moving-window 20th percentile is subtracted from the trace, bringing the fluorescence floor to ~0. The window size adapts automatically to your decay time.
' + + '2. Bandpass filter (Noise Filter toggle) \u2014 the high-pass removes slow oscillations the baseline subtraction may miss; the low-pass removes noise above what your calcium dynamics can produce.
' + + '3. Solver baseline estimate \u2014 if neither of the above fully removes the baseline, the solver estimates a scalar baseline offset during iteration.

' + + 'Together, these handle photobleaching, neuropil contamination, and slow drift without manual preprocessing.', }, // Step 10: What Deconvolved Activity IS (and IS NOT) (merged: what it is + what it is not) { From 9d86124cdf7224866568e99f9895926056e12658 Mon Sep 17 00:00:00 2001 From: daharoni Date: Fri, 6 Mar 2026 16:26:20 -0800 Subject: [PATCH 4/4] fix(catune): unify baseline confidence language in tutorial Co-Authored-By: Claude Opus 4.6 --- apps/catune/src/lib/tutorial/content/03-advanced.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/catune/src/lib/tutorial/content/03-advanced.ts b/apps/catune/src/lib/tutorial/content/03-advanced.ts index f0ad444..c0f310c 100644 --- a/apps/catune/src/lib/tutorial/content/03-advanced.ts +++ b/apps/catune/src/lib/tutorial/content/03-advanced.ts @@ -47,7 +47,7 @@ export const advancedTutorial: Tutorial = { element: '[data-tutorial="card-grid"]', title: 'Artifacts & Challenging Signals', description: - 'Common artifacts that affect fitting: Motion artifacts: sharp, symmetric deflections (not calcium-shaped). Photobleaching: slow downward baseline trend (largely handled by the automatic rolling-percentile baseline subtraction, but extreme cases may still affect results). Neuropil contamination: broad, slow signals mixed with sharp events. Motion artifacts cannot be fixed by parameter tuning \u2014 they require preprocessing. Photobleaching and neuropil contamination are partially addressed by CaTune\u2019s automatic baseline handling.

' + + 'Common artifacts that affect fitting: Motion artifacts: sharp, symmetric deflections (not calcium-shaped). Photobleaching: slow downward baseline trend (largely handled by the automatic rolling-percentile baseline subtraction, but extreme cases may still affect results). Neuropil contamination: broad, slow signals mixed with sharp events. Motion artifacts cannot be fixed by parameter tuning \u2014 they require preprocessing. Photobleaching and neuropil contamination are largely handled by CaTune\u2019s automatic baseline subtraction.

' + 'When neurons fire rapidly, calcium events overlap. The model handles this via superposition, but dense firing can make individual events hard to resolve. Under big fluorescence events, try increasing decay time to reduce dense deconvolved activity \u2014 increase as much as possible without making the fit too poor.', side: 'bottom', popoverClass: 'driver-popover--wide',