Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 1 addition & 25 deletions apps/cadecon/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ import { ConvergencePanel } from './components/charts/ConvergencePanel.tsx';
import { KernelDisplay } from './components/kernel/KernelDisplay.tsx';
import { TraceInspector } from './components/traces/TraceInspector.tsx';
import { IterationScrubber } from './components/traces/IterationScrubber.tsx';
import { AlphaDistribution } from './components/distributions/AlphaDistribution.tsx';
import { PVEDistribution } from './components/distributions/PVEDistribution.tsx';
import { EventRateDistribution } from './components/distributions/EventRateDistribution.tsx';
import { SubsetVariance } from './components/distributions/SubsetVariance.tsx';
import { SubsetDrillDown } from './components/drilldown/SubsetDrillDown.tsx';
import { SubmitPanel } from './components/community/SubmitPanel.tsx';
import { CommunityBrowser } from './components/community/CommunityBrowser.tsx';
import {
Expand All @@ -38,16 +33,14 @@ import {
loadFromBridge,
bridgeUrl,
} from './lib/data-store.ts';
import { selectedSubsetIdx, setSeed } from './lib/subset-store.ts';
import { setSeed } from './lib/subset-store.ts';
import { isRunLocked } from './lib/iteration-store.ts';

import './styles/controls.css';
import './styles/layout.css';
import './styles/distributions.css';
import './styles/trace-inspector.css';
import './styles/iteration-scrubber.css';
import './styles/kernel-display.css';
import './styles/drilldown.css';
import './styles/community.css';

function DiceIcon(): JSX.Element {
Expand Down Expand Up @@ -181,23 +174,6 @@ const App: Component = () => {
<TraceInspector />
</DashboardPanel>
</div>

{/* Row 3: Distribution Cards OR Subset Drill-Down */}
<div class="viz-grid__row viz-grid__row--bottom">
<Show
when={selectedSubsetIdx() != null}
fallback={
<div class="viz-grid__distributions">
<AlphaDistribution />
<PVEDistribution />
<EventRateDistribution />
<SubsetVariance />
</div>
}
>
<SubsetDrillDown />
</Show>
</div>
</div>
<IterationScrubber />
</VizLayout>
Expand Down
83 changes: 41 additions & 42 deletions apps/cadecon/src/components/charts/ConvergencePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,60 @@
/**
* Tabbed panel switching between Kernel Convergence, Alpha Trends, and Threshold Trends.
* Tabbed panel switching between Kernel Convergence, Alpha Trends, Threshold Trends,
* PVE Trends, and Event Rate Trends.
* All charts remain mounted (display toggled) to preserve uPlot state across tab switches.
*/

import { createSignal, type JSX } from 'solid-js';
import { createSignal, For, type JSX } from 'solid-js';
import { KernelConvergence } from './KernelConvergence.tsx';
import { AlphaTrends } from './AlphaTrends.tsx';
import { ThresholdTrends } from './ThresholdTrends.tsx';
import { PveTrends } from './PveTrends.tsx';
import { EventRateTrends } from './EventRateTrends.tsx';

type ConvergenceTab = 'kernel' | 'alpha' | 'threshold';
type ConvergenceTab = 'kernel' | 'alpha' | 'threshold' | 'pve' | 'event-rate';

interface TabEntry {
id: ConvergenceTab;
label: string;
content: () => JSX.Element;
}

const TABS: TabEntry[] = [
{ id: 'kernel', label: 'Kernel', content: () => <KernelConvergence /> },
{ id: 'alpha', label: 'Alpha', content: () => <AlphaTrends /> },
{ id: 'threshold', label: 'Threshold', content: () => <ThresholdTrends /> },
{ id: 'pve', label: 'PVE', content: () => <PveTrends /> },
{ id: 'event-rate', label: 'Event Rate', content: () => <EventRateTrends /> },
];

export function ConvergencePanel(): JSX.Element {
const [activeTab, setActiveTab] = createSignal<ConvergenceTab>('kernel');

return (
<div class="convergence-panel">
<div class="convergence-panel__tabs">
<button
class="convergence-panel__tab"
classList={{ 'convergence-panel__tab--active': activeTab() === 'kernel' }}
onClick={() => setActiveTab('kernel')}
>
Kernel
</button>
<button
class="convergence-panel__tab"
classList={{ 'convergence-panel__tab--active': activeTab() === 'alpha' }}
onClick={() => setActiveTab('alpha')}
>
Alpha
</button>
<button
class="convergence-panel__tab"
classList={{ 'convergence-panel__tab--active': activeTab() === 'threshold' }}
onClick={() => setActiveTab('threshold')}
>
Threshold
</button>
</div>
<div
class="convergence-panel__content"
style={{ display: activeTab() === 'kernel' ? 'contents' : 'none' }}
>
<KernelConvergence />
</div>
<div
class="convergence-panel__content"
style={{ display: activeTab() === 'alpha' ? 'contents' : 'none' }}
>
<AlphaTrends />
</div>
<div
class="convergence-panel__content"
style={{ display: activeTab() === 'threshold' ? 'contents' : 'none' }}
>
<ThresholdTrends />
<For each={TABS}>
{(tab) => (
<button
class="convergence-panel__tab"
classList={{ 'convergence-panel__tab--active': activeTab() === tab.id }}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
)}
</For>
</div>
<For each={TABS}>
{(tab) => (
<div
class="convergence-panel__content"
style={{ display: activeTab() === tab.id ? 'contents' : 'none' }}
>
{tab.content()}
</div>
)}
</For>
</div>
);
}
30 changes: 30 additions & 0 deletions apps/cadecon/src/components/charts/EventRateTrends.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Event Rate Trends chart: shows per-cell spike rate (Hz) evolving over iterations.
*
* Event rate = sum(sCounts) / (sCounts.length / fs). When sCounts is empty or
* fs is unavailable the rate is 0.
*/

import { createMemo, type JSX } from 'solid-js';
import { PerCellTrendsChart } from './PerCellTrendsChart.tsx';
import { samplingRate } from '../../lib/data-store.ts';

export function EventRateTrends(): JSX.Element {
const fs = createMemo(() => samplingRate() ?? 1);

return (
<PerCellTrendsChart
accessor={(entry) => {
const n = entry.sCounts.length;
if (n === 0) return 0;
let sum = 0;
for (let i = 0; i < n; i++) sum += entry.sCounts[i];
return sum / (n / fs());
}}
yLabel="Hz"
medianLabel="Median Event Rate"
medianColor="#ff7f0e"
emptyMessage="Run deconvolution to see event rate trends."
/>
);
}
53 changes: 50 additions & 3 deletions apps/cadecon/src/components/charts/KernelConvergence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import 'uplot/dist/uPlot.min.css';
import '@calab/ui/chart/chart-theme.css';
import { convergenceHistory, convergedAtIteration } from '../../lib/iteration-store.ts';
import { viewedIteration } from '../../lib/viz-store.ts';
import {
groundTruthVisible,
isDemo,
groundTruthTauRise,
groundTruthTauDecay,
} from '../../lib/data-store.ts';
import { wheelZoomPlugin, AXIS_TEXT, AXIS_GRID, AXIS_TICK } from '@calab/ui/chart';
import { convergenceMarkerPlugin } from '../../lib/chart/convergence-marker-plugin.ts';
import { viewedIterationPlugin } from '../../lib/chart/viewed-iteration-plugin.ts';
Expand All @@ -20,6 +26,41 @@ const RESIDUAL_COLOR = '#9e9e9e';
const TAU_RISE_FAINT = 'rgba(66, 165, 245, 0.3)';
const TAU_DECAY_FAINT = 'rgba(239, 83, 80, 0.3)';

/** Draw a single horizontal line at `yVal` on scale `'y'`. Caller must save/restore ctx. */
function drawHLine(ctx: CanvasRenderingContext2D, u: uPlot, yVal: number, color: string): void {
ctx.strokeStyle = color;
const yPx = u.valToPos(yVal, 'y', true);
ctx.beginPath();
ctx.moveTo(u.bbox.left, yPx);
ctx.lineTo(u.bbox.left + u.bbox.width, yPx);
ctx.stroke();
}

/** Plugin that draws horizontal dashed lines at ground truth tau_rise (blue) and tau_decay (red). */
function groundTruthPlugin(): uPlot.Plugin {
return {
hooks: {
draw(u: uPlot) {
if (!groundTruthVisible() || !isDemo()) return;
const gtTauR = groundTruthTauRise();
const gtTauD = groundTruthTauDecay();
if (gtTauR == null && gtTauD == null) return;

const dpr = devicePixelRatio;
const ctx = u.ctx;
ctx.save();
ctx.lineWidth = 1.5 * dpr;
ctx.setLineDash([6 * dpr, 4 * dpr]);

if (gtTauR != null) drawHLine(ctx, u, gtTauR * 1000, TAU_RISE_COLOR);
if (gtTauD != null) drawHLine(ctx, u, gtTauD * 1000, TAU_DECAY_COLOR);

ctx.restore();
},
},
};
}

/** Plugin that draws faint per-subset scatter circles behind the main lines. */
function subsetScatterPlugin(): uPlot.Plugin {
return {
Expand Down Expand Up @@ -57,14 +98,17 @@ function subsetScatterPlugin(): uPlot.Plugin {
export function KernelConvergence(): JSX.Element {
const [uplotRef, setUplotRef] = createSignal<uPlot | null>(null);

// Redraw when viewedIteration changes so the overlay marker updates
// Redraw when viewedIteration or ground truth visibility changes so overlay markers update
createEffect(() => {
viewedIteration(); // track
groundTruthVisible(); // track
uplotRef()?.redraw();
});

const filteredHistory = createMemo(() => convergenceHistory().filter((s) => s.iteration > 0));

const chartData = createMemo((): uPlot.AlignedData => {
const h = convergenceHistory();
const h = filteredHistory();
if (h.length === 0) return [[], [], [], []];
return [
h.map((s) => s.iteration),
Expand Down Expand Up @@ -101,6 +145,8 @@ export function KernelConvergence(): JSX.Element {
label: 'Iteration',
labelSize: 10,
labelFont: '10px sans-serif',
values: (_u: uPlot, splits: number[]) =>
splits.map((v) => (Number.isInteger(v) ? String(v) : '')),
},
{
stroke: AXIS_TEXT,
Expand All @@ -124,6 +170,7 @@ export function KernelConvergence(): JSX.Element {

const plugins = [
subsetScatterPlugin(),
groundTruthPlugin(),
convergenceMarkerPlugin(() => convergedAtIteration()),
viewedIterationPlugin(() => viewedIteration()),
wheelZoomPlugin(),
Expand All @@ -135,7 +182,7 @@ export function KernelConvergence(): JSX.Element {

return (
<Show
when={convergenceHistory().length > 0}
when={filteredHistory().length > 0}
fallback={
<div class="kernel-chart-wrapper kernel-chart-wrapper--empty">
<span>Run deconvolution to see kernel convergence.</span>
Expand Down
17 changes: 11 additions & 6 deletions apps/cadecon/src/components/charts/PerCellTrendsChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import {
import { convergenceMarkerPlugin } from '../../lib/chart/convergence-marker-plugin.ts';
import { viewedIterationPlugin } from '../../lib/chart/viewed-iteration-plugin.ts';

const IQR_FILL = 'rgba(31, 119, 180, 0.08)';
const Q_COLOR = 'rgba(31, 119, 180, 0.3)';
const IQR_FILL = 'rgba(31, 119, 180, 0.18)';
const Q_COLOR = 'rgba(31, 119, 180, 0.4)';

export interface TrendsData {
iterations: number[];
Expand Down Expand Up @@ -263,7 +263,10 @@ export interface PerCellTrendsChartProps {
export function PerCellTrendsChart(props: PerCellTrendsChartProps): JSX.Element {
const [uplotRef, setUplotRef] = createSignal<uPlot | null>(null);

const trendsData = createMemo(() => deriveTrendsData(iterationHistory(), props.accessor));
const trendsData = createMemo(() => {
const history = iterationHistory().filter((h) => h.iteration > 0);
return deriveTrendsData(history, props.accessor);
});

// Redraw when overlay state changes
createEffect(() => {
Expand All @@ -287,8 +290,8 @@ export function PerCellTrendsChart(props: PerCellTrendsChartProps): JSX.Element
width: 2,
points: { show: true, size: 5 },
},
{ label: 'Q25', stroke: Q_COLOR, width: 1, dash: [4, 2], show: false },
{ label: 'Q75', stroke: Q_COLOR, width: 1, dash: [4, 2], show: false },
{ label: 'Q25', stroke: Q_COLOR, width: 1, dash: [4, 2] },
{ label: 'Q75', stroke: Q_COLOR, width: 1, dash: [4, 2] },
];

const scales = createMemo((): uPlot.Scales => {
Expand All @@ -308,6 +311,8 @@ export function PerCellTrendsChart(props: PerCellTrendsChartProps): JSX.Element
label: 'Iteration',
labelSize: 10,
labelFont: '10px sans-serif',
values: (_u: uPlot, splits: number[]) =>
splits.map((v) => (Number.isInteger(v) ? String(v) : '')),
},
{
stroke: AXIS_TEXT,
Expand All @@ -333,7 +338,7 @@ export function PerCellTrendsChart(props: PerCellTrendsChartProps): JSX.Element

return (
<Show
when={iterationHistory().length > 0}
when={trendsData().iterations.length > 0}
fallback={
<div class="kernel-chart-wrapper kernel-chart-wrapper--empty">
<span>{props.emptyMessage}</span>
Expand Down
18 changes: 18 additions & 0 deletions apps/cadecon/src/components/charts/PveTrends.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* PVE Trends chart: shows per-cell percent variance explained evolving over iterations.
*/

import type { JSX } from 'solid-js';
import { PerCellTrendsChart } from './PerCellTrendsChart.tsx';

export function PveTrends(): JSX.Element {
return (
<PerCellTrendsChart
accessor={(entry) => entry.pve}
yLabel="PVE"
medianLabel="Median PVE"
medianColor="#2ca02c"
emptyMessage="Run deconvolution to see PVE trends."
/>
);
}
10 changes: 8 additions & 2 deletions apps/cadecon/src/lib/algorithm-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import { samplingRate } from './data-store.ts';

// --- Algorithm parameter signals ---

const [tauRiseInit, setTauRiseInit] = createSignal(0.1);
const [tauDecayInit, setTauDecayInit] = createSignal(0.6);
// Initial kernel time constants (seconds). These MUST start at or above the
// true values. The iterative solver can only refine time constants downward —
// a too-fast kernel is compensated by placing extra spikes, so the free-form
// kernel never receives an error signal pushing it slower. Starting from
// conservatively slow values ensures the optimizer passes through the optimum
// on the way down, where best-residual tracking (iteration-manager.ts) catches it.
const [tauRiseInit, setTauRiseInit] = createSignal(0.2);
const [tauDecayInit, setTauDecayInit] = createSignal(1.0);
const [upsampleTarget, setUpsampleTarget] = createSignal(300);
const [weightingEnabled, setWeightingEnabled] = createSignal(false);
const [hpFilterEnabled, setHpFilterEnabled] = createSignal(true);
Expand Down
Loading
Loading