/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
timeseries-compressibility / src / App.tsx
230 lines · 8.3 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 TrueRateCommand from './components/TrueRateCommand'
8import { DEFAULT_SPEC, clampSpec, designKernel, kernelNorm } from './model/filters'
9import { theoreticalRateBits } from './model/theory'
10import { LATENT_SEED } from './model/latent'
11import { DEFAULT_LPC_ORDER, LPC_ORDERS } from './compress/codecs'
12import type { BoundResult, CodecResult } from './compress/codecs'
13import type { CompressRequest, CompressResponse } from './worker/compressWorker'
15const BLOCK_SIZES = [10000, 20000, 50000, 100000, 200000, 500000, 1000000]
16const DEFAULT_BLOCK_SIZE = 100000
18interface CompressionState {
19 results: CodecResult[]
20 bounds: BoundResult[]
21 empiricalStd: number
22 computing: boolean
23 error: string | null
26/** The nine codec sizes, measured in a worker on a debounced parameter set. */
27function useCompression(
28 kernel: Float64Array,
29 sigma: number,
30 dither: boolean,
31 lpcOrder: number,
32 blockSize: number,
33): CompressionState {
34 const [state, setState] = useState<CompressionState>({
35 results: [],
36 bounds: [],
37 empiricalStd: 0,
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,
52 bounds: e.data.error ? [] : e.data.bounds,
53 empiricalStd: e.data.empiricalStd,
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,
74 blockSize,
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,
79 }
80 workerRef.current?.postMessage(request)
81 }, 250)
82 return () => clearTimeout(timer)
83 }, [kernel, sigma, dither, lpcOrder, blockSize])
85 return state
88export default function App() {
89 const [sigma, setSigma] = useState(5)
90 const [sampleRateHz, setSampleRateHz] = useState(30000)
91 const [spec, setSpec] = useState(DEFAULT_SPEC)
92 const [dither, setDither] = useState(false)
93 const [lpcOrder, setLpcOrder] = useState(DEFAULT_LPC_ORDER)
94 const [blockSize, setBlockSize] = useState(DEFAULT_BLOCK_SIZE)
96 const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
97 const sigmaY = useMemo(() => {
98 const filtered = sigma * kernelNorm(kernel)
99 return dither ? Math.sqrt(filtered * filtered + 1 / 12) : filtered
100 }, [kernel, sigma, dither])
101 const theoryBits = useMemo(() => theoreticalRateBits(kernel, sigma, dither), [kernel, sigma, dither])
102 const compression = useCompression(kernel, sigma, dither, lpcOrder, blockSize)
104 return (
105 <div className="app">
106 <header className="app-header">
107 <h1>Time-series compressibility</h1>
108 <p>
109 Gaussian noise → FIR filter → optional dither → round to integers. How well can the
110 integer stream be losslessly compressed, and does the spectral entropy-rate formula
111 predict the limit?
112 </p>
113 </header>
115 {/* The controls stay pinned so the parameters and the ratios they move
116 are always on screen together, whatever is scrolled to. */}
117 <section className="card control-bar">
118 <Controls
119 sigma={sigma}
120 setSigma={setSigma}
121 sampleRateHz={sampleRateHz}
122 setSampleRateHz={rate => {
123 // Band edges are absolute, so a new rate can push them past
124 // Nyquist; re-snap the spec so sliders and kernel stay in step.
125 setSampleRateHz(rate)
126 setSpec(s => clampSpec(s, rate))
127 }}
128 spec={spec}
129 setSpec={setSpec}
130 dither={dither}
131 setDither={setDither}
132 />
133 </section>
135 <section className="card">
136 <h2>Compression</h2>
137 <div className="stat-row">
138 <div className="stat">
139 <span className="label">predicted std of z</span>
140 <span className="value">
141 {sigmaY.toFixed(2)} <small>steps</small>
142 </span>
143 </div>
144 <div className="stat">
145 <span className="label">measured std of z</span>
146 <span className="value">
147 {compression.results.length > 0 ? compression.empiricalStd.toFixed(2) : '…'}{' '}
148 <small>steps</small>
149 </span>
150 </div>
151 <div className="stat">
152 <span className="label">theoretical rate R</span>
153 <span className="value">
154 {theoryBits.toFixed(2)} <small>bits/sample</small>
155 </span>
156 </div>
157 <div className="stat">
158 <span className="label">implied best ratio</span>
159 <span className="value">{theoryBits > 0 ? `${(16 / theoryBits).toFixed(2)}×` : '—'}</span>
160 </div>
161 </div>
162 {/* Settings of the measurement, not of the model — so they live with
163 the chart they change rather than in the model bar. */}
164 <div className="measure-row">
165 <label>
166 LPC order
167 <select value={lpcOrder} onChange={e => setLpcOrder(Number(e.target.value))}>
168 {LPC_ORDERS.map(o => (
169 <option key={o} value={o}>
170 {o}
171 </option>
172 ))}
173 </select>
174 </label>
175 <label>
176 block size
177 <select value={blockSize} onChange={e => setBlockSize(Number(e.target.value))}>
178 {BLOCK_SIZES.map(n => (
179 <option key={n} value={n}>
180 {n.toLocaleString()} samples
181 </option>
182 ))}
183 </select>
184 </label>
185 </div>
186 {compression.error ? (
187 <p className="card-note">Compression failed: {compression.error}</p>
188 ) : (
189 <CompressionChart
190 results={compression.results}
191 bounds={compression.bounds}
192 theoryBits={theoryBits}
193 computing={compression.computing}
194 />
195 )}
196 <p className="card-note">
197 Measured on a {blockSize.toLocaleString()}-sample block of the same latent data the
198 signal view shows; sizes include everything a decoder needs (ANS symbol table, LPC
199 coefficients). Baseline is raw int16 (16 bits/sample). The hollow bar in each group is
200 that group's entropy limit — the order-0 entropy of the stream being coded, which no
201 per-sample entropy coder can beat and ANS falls short of by its symbol table plus its
202 own arithmetic loss. The dashed line is the theoretical rate R from the spectral
203 formula in the math section — approximate where quantization dominates the spectrum
204 (see the S(f) = 1 threshold on the response plot).
205 </p>
206 <TrueRateCommand sigma={sigma} spec={spec} sampleRateHz={sampleRateHz} dither={dither} />
207 </section>
209 <section className="card">
210 <h2>Quantized signal z</h2>
211 <ScrollingView kernel={kernel} sigma={sigma} dither={dither} sigmaY={sigmaY} />
212 <p className="card-note">
213 A window of samples from the model, drawn from a fixed latent noise sequence — changing
214 σ, the filter, or dither transforms the same underlying data, so the trace morphs
215 rather than resampling. Press play to advance through the sequence.
216 </p>
217 </section>
219 <section className="card">
220 <h2>Filter</h2>
221 <FilterViz kernel={kernel} sampleRateHz={sampleRateHz} sigma={sigma} />
222 </section>
224 <section className="card">
225 <h2>The math</h2>
226 <MathSection />
227 </section>
228 </div>
229 )
moveopenescclose