/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
timeseries-compressibility / src / App.tsx
179 lines · 6.2 KBBlameHistoryRaw
1import { useEffect, useMemo, useRef, useState } from 'react'
2import Controls from './components/Controls'
3import FilterViz from './components/FilterViz'
4import ScrollingView from './components/ScrollingView'
5import CompressionChart from './components/CompressionChart'
6import MathSection from './components/MathSection'
7import { DEFAULT_SPEC, designKernel, kernelNorm } from './model/filters'
8import { theoreticalRateBits } from './model/theory'
9import type { CodecResult } from './compress/codecs'
10import type { CompressRequest, CompressResponse } from './worker/compressWorker'
12const BLOCK_SIZE = 120000
13const BLOCK_SEED = 20260729
15interface CompressionState {
16 results: CodecResult[]
17 empiricalStd: number
18 computing: boolean
19 error: string | null
22/** The nine codec sizes, measured in a worker on a debounced parameter set. */
23function useCompression(kernel: Float64Array, sigma: number, dither: boolean): CompressionState {
24 const [state, setState] = useState<CompressionState>({
25 results: [],
26 empiricalStd: 0,
27 computing: true,
28 error: null,
29 })
30 const workerRef = useRef<Worker | null>(null)
31 const idRef = useRef(0)
33 useEffect(() => {
34 const worker = new Worker(new URL('./worker/compressWorker.ts', import.meta.url), {
35 type: 'module',
36 })
37 worker.onmessage = (e: MessageEvent<CompressResponse>) => {
38 if (e.data.id !== idRef.current) return
39 setState({
40 results: e.data.error ? [] : e.data.results,
41 empiricalStd: e.data.empiricalStd,
42 computing: false,
43 error: e.data.error ?? null,
44 })
45 }
46 workerRef.current = worker
47 return () => {
48 worker.terminate()
49 workerRef.current = null
50 }
51 }, [])
53 useEffect(() => {
54 setState(s => ({ ...s, computing: true }))
55 const id = ++idRef.current
56 const timer = setTimeout(() => {
57 const request: CompressRequest = {
58 id,
59 kernel,
60 sigma,
61 dither,
62 blockSize: BLOCK_SIZE,
63 seed: BLOCK_SEED,
64 }
65 workerRef.current?.postMessage(request)
66 }, 250)
67 return () => clearTimeout(timer)
68 }, [kernel, sigma, dither])
70 return state
73export default function App() {
74 const [sigma, setSigma] = useState(5)
75 const [sampleRateHz, setSampleRateHz] = useState(30000)
76 const [spec, setSpec] = useState(DEFAULT_SPEC)
77 const [dither, setDither] = useState(false)
79 const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
80 const sigmaY = useMemo(() => {
81 const filtered = sigma * kernelNorm(kernel)
82 return dither ? Math.sqrt(filtered * filtered + 1 / 12) : filtered
83 }, [kernel, sigma, dither])
84 const theoryBits = useMemo(() => theoreticalRateBits(kernel, sigma, dither), [kernel, sigma, dither])
85 const compression = useCompression(kernel, sigma, dither)
87 return (
88 <div className="app">
89 <header className="app-header">
90 <h1>Time-series compressibility</h1>
91 <p>
92 Gaussian noise → FIR filter → optional dither → round to integers. How well can the
93 integer stream be losslessly compressed, and does the spectral entropy-rate formula
94 predict the limit?
95 </p>
96 </header>
98 {/* The controls stay pinned so the parameters and the ratios they move
99 are always on screen together, whatever is scrolled to. */}
100 <section className="card control-bar">
101 <Controls
102 sigma={sigma}
103 setSigma={setSigma}
104 sampleRateHz={sampleRateHz}
105 setSampleRateHz={setSampleRateHz}
106 spec={spec}
107 setSpec={setSpec}
108 dither={dither}
109 setDither={setDither}
110 />
111 </section>
113 <section className="card">
114 <h2>Compression</h2>
115 <div className="stat-row">
116 <div className="stat">
117 <span className="label">predicted std of z</span>
118 <span className="value">
119 {sigmaY.toFixed(2)} <small>steps</small>
120 </span>
121 </div>
122 <div className="stat">
123 <span className="label">measured std of z</span>
124 <span className="value">
125 {compression.results.length > 0 ? compression.empiricalStd.toFixed(2) : '…'}{' '}
126 <small>steps</small>
127 </span>
128 </div>
129 <div className="stat">
130 <span className="label">theoretical rate R</span>
131 <span className="value">
132 {theoryBits.toFixed(2)} <small>bits/sample</small>
133 </span>
134 </div>
135 <div className="stat">
136 <span className="label">implied best ratio</span>
137 <span className="value">{theoryBits > 0 ? `${(16 / theoryBits).toFixed(2)}×` : '—'}</span>
138 </div>
139 </div>
140 {compression.error ? (
141 <p className="card-note">Compression failed: {compression.error}</p>
142 ) : (
143 <CompressionChart
144 results={compression.results}
145 theoryBits={theoryBits}
146 computing={compression.computing}
147 />
148 )}
149 <p className="card-note">
150 Measured on a {BLOCK_SIZE.toLocaleString()}-sample block of the same latent data the
151 signal view shows; sizes include everything a decoder needs (ANS symbol table, LPC
152 coefficients). Baseline is raw int16 (16 bits/sample). The dashed line is the
153 theoretical rate R from the spectral formula in the math section — approximate where
154 quantization dominates the spectrum (see the S(f) = 1 threshold on the response plot).
155 </p>
156 </section>
158 <section className="card">
159 <h2>Quantized signal z</h2>
160 <ScrollingView kernel={kernel} sigma={sigma} dither={dither} sigmaY={sigmaY} />
161 <p className="card-note">
162 A window of samples from the model, drawn from a fixed latent noise sequence — changing
163 σ, the filter, or dither transforms the same underlying data, so the trace morphs
164 rather than resampling. Press play to advance through the sequence.
165 </p>
166 </section>
168 <section className="card">
169 <h2>Filter</h2>
170 <FilterViz kernel={kernel} sampleRateHz={sampleRateHz} sigma={sigma} />
171 </section>
173 <section className="card">
174 <h2>The math</h2>
175 <MathSection />
176 </section>
177 </div>
178 )
moveopenescclose