/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
timeseries-compressibility / src / App.tsx
288 lines · 10.9 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'
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 7import MethodNote from './components/MethodNote'
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 8import { useEntropyRate } from './components/useEntropyRate'
fec2284Show analytic entropy-rate prediction alongside the Monte-Carlo estimateJeremy Magland 9import { predictEntropyRate } from './entropy'
fb1a12fSnap every parameter slider to a ladder of round valuesJeremy Magland 10import { DEFAULT_SPEC, clampSpec, designKernel, kernelNorm } from './model/filters'
e411dffMake LPC order and compression block size controlsJeremy Magland 11import { LATENT_SEED } from './model/latent'
12import { DEFAULT_LPC_ORDER, LPC_ORDERS } from './compress/codecs'
4ec0133Code against the prediction, not the integer residualJeremy Magland 13import type { CodecResult } from './compress/codecs'
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 14import type { CompressRequest, CompressResponse } from './worker/compressWorker'
e411dffMake LPC order and compression block size controlsJeremy Magland 16const BLOCK_SIZES = [10000, 20000, 50000, 100000, 200000, 500000, 1000000]
17const DEFAULT_BLOCK_SIZE = 100000
19interface CompressionState {
20 results: CodecResult[]
21 empiricalStd: number
22 computing: boolean
23 error: string | null
4ec0133Code against the prediction, not the integer residualJeremy Magland 26/** The ten 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 lpcOrder: number,
31 blockSize: number,
32): CompressionState {
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 33 const [state, setState] = useState<CompressionState>({
34 results: [],
35 empiricalStd: 0,
36 computing: true,
37 error: null,
38 })
39 const workerRef = useRef<Worker | null>(null)
40 const idRef = useRef(0)
42 useEffect(() => {
43 const worker = new Worker(new URL('./worker/compressWorker.ts', import.meta.url), {
44 type: 'module',
45 })
46 worker.onmessage = (e: MessageEvent<CompressResponse>) => {
47 if (e.data.id !== idRef.current) return
48 setState({
49 results: e.data.error ? [] : e.data.results,
50 empiricalStd: e.data.empiricalStd,
51 computing: false,
52 error: e.data.error ?? null,
53 })
54 }
55 workerRef.current = worker
56 return () => {
57 worker.terminate()
58 workerRef.current = null
59 }
60 }, [])
62 useEffect(() => {
63 setState(s => ({ ...s, computing: true }))
64 const id = ++idRef.current
65 const timer = setTimeout(() => {
66 const request: CompressRequest = {
67 id,
68 kernel,
69 sigma,
71 lpcOrder,
72 // The same seed the signal view draws from, so the block really is
73 // the data on screen.
74 seed: LATENT_SEED,
76 workerRef.current?.postMessage(request)
77 }, 250)
78 return () => clearTimeout(timer)
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 79 }, [kernel, sigma, lpcOrder, blockSize])
81 return state
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 85 * The terminal command that estimates the entropy rate R at the current
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 86 * settings, using the unbiased Monte-Carlo estimator from the companion
87 * timeseries-entropy package.
88 */
89function mcCommand(sigma: number, spec: ReturnType<typeof clampSpec>, rate: number): string {
90 const parts = [
91 'uvx --from git+https://github.com/concept-collection/timeseries-entropy',
92 'timeseries-entropy',
93 `--sigma ${sigma}`,
94 ]
96 case 'none':
97 parts.push('--filter none')
98 break
99 case 'movingAverage':
100 parts.push('--filter moving-average', `--width ${spec.width}`)
101 break
102 case 'lowpass':
103 parts.push('--filter lowpass', `--high ${spec.highHz}`, `--taps ${spec.taps}`, `--rate ${rate}`)
104 break
105 case 'bandpass':
106 parts.push(
107 '--filter bandpass',
108 `--low ${spec.lowHz}`,
109 `--high ${spec.highHz}`,
110 `--taps ${spec.taps}`,
111 `--rate ${rate}`,
112 )
113 break
114 case 'firstDifference':
115 parts.push('--filter first-difference')
116 break
117 }
118 return parts.join(' ')
122 const [sigma, setSigma] = useState(5)
123 const [sampleRateHz, setSampleRateHz] = useState(30000)
124 const [spec, setSpec] = useState(DEFAULT_SPEC)
e411dffMake LPC order and compression block size controlsJeremy Magland 125 const [lpcOrder, setLpcOrder] = useState(DEFAULT_LPC_ORDER)
126 const [blockSize, setBlockSize] = useState(DEFAULT_BLOCK_SIZE)
128 const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 129 const sigmaY = useMemo(() => sigma * kernelNorm(kernel), [kernel, sigma])
130 const compression = useCompression(kernel, sigma, lpcOrder, blockSize)
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 131 const entropyRate = useEntropyRate(kernel, sigma)
fec2284Show analytic entropy-rate prediction alongside the Monte-Carlo estimateJeremy Magland 132 const theoryBits = useMemo(() => predictEntropyRate(kernel, sigma), [kernel, sigma])
134 return (
135 <div className="app">
136 <header className="app-header">
137 <h1>Time-series compressibility</h1>
138 <p>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 139 Gaussian noise → FIR filter → round to integers. How well can the integer stream be
140 losslessly compressed, and how close do practical codecs get to the entropy rate of
141 the process?
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 />
161 </section>
163 <section className="card">
164 <h2>Compression</h2>
fec2284Show analytic entropy-rate prediction alongside the Monte-Carlo estimateJeremy Magland 165 {/* Two readouts of the same number, each keyed to its chart line by a
166 sample of that line's own stroke. */}
168 <div className="stat">
170 <span className="line-swatch" style={{ borderTop: '2px dotted var(--theory)' }} />
171 entropy rate R — analytic theory
173 <span className="value">
fec2284Show analytic entropy-rate prediction alongside the Monte-Carlo estimateJeremy Magland 174 {theoryBits.toFixed(2)} <small>bits/sample</small>
175 </span>
176 <span className="stat-sub">
177 {theoryBits > 0 ? `best possible ratio ${(16 / theoryBits).toFixed(2)}×` : '—'}
179 </div>
180 <div className="stat">
182 <span className="line-swatch" style={{ borderTop: '2px dashed var(--ink-2)' }} />
183 entropy rate R — Monte-Carlo ground truth
184 </span>
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 186 {entropyRate.mean !== null ? entropyRate.mean.toFixed(2) : '—'}
187 {entropyRate.se !== null && <small> ± {entropyRate.se.toFixed(2)}</small>}{' '}
3f857e3Estimate the reference rate R in the browserJeremy Magland 188 <small>bits/sample</small>
189 </span>
191 {entropyRate.mean !== null && entropyRate.mean > 0
192 ? `best possible ratio ${(16 / entropyRate.mean).toFixed(2)}×`
193 : 'run the estimate to check the theory'}
194 </span>
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 195 {/* The estimate lives with its readout: start, watch it refine,
196 stop; a model change resets it. */}
197 <span className="stat-action">
198 <button onClick={entropyRate.running ? entropyRate.stop : entropyRate.start}>
199 {entropyRate.running ? 'stop' : entropyRate.perPast.length > 0 ? 'refine' : 'estimate'}
200 </button>
201 {/* Each independent past contributes one unbiased estimate;
202 the readout is their average, so that is the word used. */}
203 <span className="estimate-status">
204 {entropyRate.running
205 ? `${entropyRate.perPast.length} estimates · ${entropyRate.progress ?? 'starting…'}`
206 : entropyRate.perPast.length > 0
207 ? `${entropyRate.perPast.length} estimates · M = ${entropyRate.past}`
208 : `M = ${entropyRate.past}`}
209 </span>
210 </span>
e411dffMake LPC order and compression block size controlsJeremy Magland 213 {/* Settings of the measurement, not of the model — so they live with
214 the chart they change rather than in the model bar. */}
215 <div className="measure-row">
216 <label>
217 LPC order
218 <select value={lpcOrder} onChange={e => setLpcOrder(Number(e.target.value))}>
219 {LPC_ORDERS.map(o => (
220 <option key={o} value={o}>
221 {o}
222 </option>
223 ))}
224 </select>
225 </label>
226 <label>
227 block size
228 <select value={blockSize} onChange={e => setBlockSize(Number(e.target.value))}>
229 {BLOCK_SIZES.map(n => (
230 <option key={n} value={n}>
231 {n.toLocaleString()} samples
232 </option>
233 ))}
234 </select>
235 </label>
236 </div>
238 <p className="card-note">Compression failed: {compression.error}</p>
239 ) : (
240 <CompressionChart
241 results={compression.results}
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 242 rateBits={entropyRate.mean}
244 theoryBits={theoryBits}
246 />
247 )}
248 <p className="card-note">
e411dffMake LPC order and compression block size controlsJeremy Magland 249 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 250 signal view shows; sizes include everything a decoder needs (ANS symbol table, LPC
fec2284Show analytic entropy-rate prediction alongside the Monte-Carlo estimateJeremy Magland 251 coefficients). Baseline is raw int16 (16 bits/sample). The two reference lines mark
252 the entropy rate R of the process — the one limit no lossless method whatsoever can
253 beat: dotted for the analytic theory, dashed for the Monte-Carlo ground truth, shaded
254 by its standard error (see the method section at the bottom).
4ec0133Code against the prediction, not the integer residualJeremy Magland 255 What separates the methods is the model each one codes against: ANS uses the histogram
256 of whatever stream it is given, so a better prefilter is the only way it improves,
257 while the conditional-Gaussian coder codes each sample against a prediction and can
258 therefore approach R. The strip under the chart scores each coder against its own
259 model, which is a question about the coder rather than about the model.
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 262 label="cross-check R from the command line:"
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 263 command={mcCommand(sigma, spec, sampleRateHz)}
268 <h2>Quantized signal z</h2>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 269 <ScrollingView kernel={kernel} sigma={sigma} sigmaY={sigmaY} />
271 A window of samples from the model, drawn from a fixed latent noise sequence — changing
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 272 σ or the filter transforms the same underlying data, so the trace morphs rather than
273 resampling. Press play to advance through the sequence.
275 </section>
277 <section className="card">
278 <h2>Filter</h2>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 279 <FilterViz kernel={kernel} sampleRateHz={sampleRateHz} />
286 </div>
287 )
moveopenescclose