/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
timeseries-compressibility / src / App.tsx
263 lines · 9.4 KBCodeBlameHistory
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 1import { useEffect, useMemo, useRef, useState } from 'react'
95929e6Monte-Carlo ground-truth script for R, with the command printed in the UIJeremy Magland 2import CopyableCommand from './components/CopyableCommand'
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 3import Controls from './components/Controls'
4import FilterViz from './components/FilterViz'
5import ScrollingView from './components/ScrollingView'
6import CompressionChart from './components/CompressionChart'
7import MathSection from './components/MathSection'
fb1a12fSnap every parameter slider to a ladder of round valuesJeremy Magland 8import { DEFAULT_SPEC, clampSpec, designKernel, kernelNorm } from './model/filters'
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 9import { theoreticalRateBits } from './model/theory'
e411dffMake LPC order and compression block size controlsJeremy Magland 10import { LATENT_SEED } from './model/latent'
11import { DEFAULT_LPC_ORDER, LPC_ORDERS } from './compress/codecs'
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 12import type { BoundResult, CodecResult } from './compress/codecs'
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 13import type { CompressRequest, CompressResponse } from './worker/compressWorker'
e411dffMake LPC order and compression block size controlsJeremy Magland 15const BLOCK_SIZES = [10000, 20000, 50000, 100000, 200000, 500000, 1000000]
16const DEFAULT_BLOCK_SIZE = 100000
18interface CompressionState {
19 results: CodecResult[]
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 20 bounds: BoundResult[]
22 computing: boolean
23 error: string | null
26/** The nine codec sizes, measured in a worker on a debounced parameter set. */
e411dffMake LPC order and compression block size controlsJeremy Magland 27function useCompression(
28 kernel: Float64Array,
29 sigma: number,
30 dither: boolean,
31 lpcOrder: number,
32 blockSize: number,
33): CompressionState {
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 34 const [state, setState] = useState<CompressionState>({
35 results: [],
38 computing: true,
39 error: null,
40 })
41 const workerRef = useRef<Worker | null>(null)
42 const idRef = useRef(0)
44 useEffect(() => {
45 const worker = new Worker(new URL('./worker/compressWorker.ts', import.meta.url), {
46 type: 'module',
47 })
48 worker.onmessage = (e: MessageEvent<CompressResponse>) => {
49 if (e.data.id !== idRef.current) return
50 setState({
51 results: e.data.error ? [] : e.data.results,
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 52 bounds: e.data.error ? [] : e.data.bounds,
54 computing: false,
55 error: e.data.error ?? null,
56 })
57 }
58 workerRef.current = worker
59 return () => {
60 worker.terminate()
61 workerRef.current = null
62 }
63 }, [])
65 useEffect(() => {
66 setState(s => ({ ...s, computing: true }))
67 const id = ++idRef.current
68 const timer = setTimeout(() => {
69 const request: CompressRequest = {
70 id,
71 kernel,
72 sigma,
73 dither,
75 lpcOrder,
76 // The same seed the signal view draws from, so the block really is
77 // the data on screen.
78 seed: LATENT_SEED,
80 workerRef.current?.postMessage(request)
81 }, 250)
82 return () => clearTimeout(timer)
e411dffMake LPC order and compression block size controlsJeremy Magland 83 }, [kernel, sigma, dither, lpcOrder, blockSize])
85 return state
95929e6Monte-Carlo ground-truth script for R, with the command printed in the UIJeremy Magland 88/** The terminal command for scripts/true_rate.py at the current settings. */
89function mcCommand(sigma: number, spec: ReturnType<typeof clampSpec>, rate: number, dither: boolean): string {
90 const parts = ['python scripts/true_rate.py', `--sigma ${sigma}`]
91 switch (spec.family) {
92 case 'none':
93 parts.push('--filter none')
94 break
95 case 'movingAverage':
96 parts.push('--filter moving-average', `--width ${spec.width}`)
97 break
98 case 'lowpass':
99 parts.push('--filter lowpass', `--high ${spec.highHz}`, `--taps ${spec.taps}`, `--rate ${rate}`)
100 break
101 case 'bandpass':
102 parts.push(
103 '--filter bandpass',
104 `--low ${spec.lowHz}`,
105 `--high ${spec.highHz}`,
106 `--taps ${spec.taps}`,
107 `--rate ${rate}`,
108 )
109 break
110 case 'firstDifference':
111 parts.push('--filter first-difference')
112 break
113 }
114 if (dither) parts.push('--dither')
115 return parts.join(' ')
119 const [sigma, setSigma] = useState(5)
120 const [sampleRateHz, setSampleRateHz] = useState(30000)
121 const [spec, setSpec] = useState(DEFAULT_SPEC)
122 const [dither, setDither] = useState(false)
e411dffMake LPC order and compression block size controlsJeremy Magland 123 const [lpcOrder, setLpcOrder] = useState(DEFAULT_LPC_ORDER)
124 const [blockSize, setBlockSize] = useState(DEFAULT_BLOCK_SIZE)
126 const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
127 const sigmaY = useMemo(() => {
128 const filtered = sigma * kernelNorm(kernel)
129 return dither ? Math.sqrt(filtered * filtered + 1 / 12) : filtered
130 }, [kernel, sigma, dither])
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 131 const theoryBits = useMemo(() => theoreticalRateBits(kernel, sigma, dither), [kernel, sigma, dither])
e411dffMake LPC order and compression block size controlsJeremy Magland 132 const compression = useCompression(kernel, sigma, dither, lpcOrder, blockSize)
134 return (
135 <div className="app">
136 <header className="app-header">
137 <h1>Time-series compressibility</h1>
138 <p>
139 Gaussian noise → FIR filter → optional dither → round to integers. How well can the
140 integer stream be losslessly compressed, and does the spectral entropy-rate formula
141 predict the limit?
142 </p>
143 </header>
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 145 {/* The controls stay pinned so the parameters and the ratios they move
146 are always on screen together, whatever is scrolled to. */}
147 <section className="card control-bar">
149 sigma={sigma}
150 setSigma={setSigma}
151 sampleRateHz={sampleRateHz}
fb1a12fSnap every parameter slider to a ladder of round valuesJeremy Magland 152 setSampleRateHz={rate => {
153 // Band edges are absolute, so a new rate can push them past
154 // Nyquist; re-snap the spec so sliders and kernel stay in step.
155 setSampleRateHz(rate)
156 setSpec(s => clampSpec(s, rate))
157 }}
159 setSpec={setSpec}
160 dither={dither}
161 setDither={setDither}
162 />
163 </section>
165 <section className="card">
166 <h2>Compression</h2>
167 <div className="stat-row">
168 <div className="stat">
169 <span className="label">predicted std of z</span>
170 <span className="value">
171 {sigmaY.toFixed(2)} <small>steps</small>
172 </span>
173 </div>
174 <div className="stat">
175 <span className="label">measured std of z</span>
176 <span className="value">
177 {compression.results.length > 0 ? compression.empiricalStd.toFixed(2) : '…'}{' '}
178 <small>steps</small>
179 </span>
180 </div>
181 <div className="stat">
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 182 <span className="label">theoretical rate R</span>
184 {theoryBits.toFixed(2)} <small>bits/sample</small>
185 </span>
186 </div>
187 <div className="stat">
188 <span className="label">implied best ratio</span>
189 <span className="value">{theoryBits > 0 ? `${(16 / theoryBits).toFixed(2)}×` : '—'}</span>
190 </div>
191 </div>
e411dffMake LPC order and compression block size controlsJeremy Magland 192 {/* Settings of the measurement, not of the model — so they live with
193 the chart they change rather than in the model bar. */}
194 <div className="measure-row">
195 <label>
196 LPC order
197 <select value={lpcOrder} onChange={e => setLpcOrder(Number(e.target.value))}>
198 {LPC_ORDERS.map(o => (
199 <option key={o} value={o}>
200 {o}
201 </option>
202 ))}
203 </select>
204 </label>
205 <label>
206 block size
207 <select value={blockSize} onChange={e => setBlockSize(Number(e.target.value))}>
208 {BLOCK_SIZES.map(n => (
209 <option key={n} value={n}>
210 {n.toLocaleString()} samples
211 </option>
212 ))}
213 </select>
214 </label>
215 </div>
217 <p className="card-note">Compression failed: {compression.error}</p>
218 ) : (
219 <CompressionChart
220 results={compression.results}
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 221 bounds={compression.bounds}
223 computing={compression.computing}
224 />
225 )}
226 <p className="card-note">
e411dffMake LPC order and compression block size controlsJeremy Magland 227 Measured on a {blockSize.toLocaleString()}-sample block of the same latent data the
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 228 signal view shows; sizes include everything a decoder needs (ANS symbol table, LPC
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 229 coefficients). Baseline is raw int16 (16 bits/sample). The hollow bar in each group is
230 that group's entropy limit — the order-0 entropy of the stream being coded, which no
231 per-sample entropy coder can beat and ANS falls short of by its symbol table plus its
232 own arithmetic loss. The dashed line is the theoretical rate R from the spectral
233 formula in the math section — approximate where quantization dominates the spectrum
234 (see the S(f) = 1 threshold on the response plot).
237 label="check R against a Monte-Carlo ground truth (runs locally, ~2 min):"
238 command={mcCommand(sigma, spec, sampleRateHz, dither)}
239 />
243 <h2>Quantized signal z</h2>
244 <ScrollingView kernel={kernel} sigma={sigma} dither={dither} sigmaY={sigmaY} />
245 <p className="card-note">
246 A window of samples from the model, drawn from a fixed latent noise sequence — changing
247 σ, the filter, or dither transforms the same underlying data, so the trace morphs
248 rather than resampling. Press play to advance through the sequence.
249 </p>
250 </section>
252 <section className="card">
253 <h2>Filter</h2>
254 <FilterViz kernel={kernel} sampleRateHz={sampleRateHz} sigma={sigma} />
255 </section>
258 <h2>The math</h2>
259 <MathSection />
260 </section>
261 </div>
262 )
moveopenescclose