/ proof-of-concept / timeseries-compressibility
Sign in
proof-of-concept / timeseries-compressibility
timeseries-compressibility / src / App.tsx
282 lines · 10.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'
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'
fb1a12fSnap every parameter slider to a ladder of round valuesJeremy Magland 9import { DEFAULT_SPEC, clampSpec, designKernel, kernelNorm } from './model/filters'
e411dffMake LPC order and compression block size controlsJeremy Magland 10import { LATENT_SEED } from './model/latent'
11import { DEFAULT_LPC_ORDER, LPC_ORDERS } from './compress/codecs'
4ec0133Code against the prediction, not the integer residualJeremy Magland 12import type { 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[]
20 empiricalStd: number
21 computing: boolean
22 error: string | null
4ec0133Code against the prediction, not the integer residualJeremy Magland 25/** The ten codec sizes, measured in a worker on a debounced parameter set. */
e411dffMake LPC order and compression block size controlsJeremy Magland 26function useCompression(
27 kernel: Float64Array,
28 sigma: number,
29 lpcOrder: number,
30 blockSize: number,
31): CompressionState {
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 32 const [state, setState] = useState<CompressionState>({
33 results: [],
34 empiricalStd: 0,
35 computing: true,
36 error: null,
37 })
38 const workerRef = useRef<Worker | null>(null)
39 const idRef = useRef(0)
41 useEffect(() => {
42 const worker = new Worker(new URL('./worker/compressWorker.ts', import.meta.url), {
43 type: 'module',
44 })
45 worker.onmessage = (e: MessageEvent<CompressResponse>) => {
46 if (e.data.id !== idRef.current) return
47 setState({
48 results: e.data.error ? [] : e.data.results,
49 empiricalStd: e.data.empiricalStd,
50 computing: false,
51 error: e.data.error ?? null,
52 })
53 }
54 workerRef.current = worker
55 return () => {
56 worker.terminate()
57 workerRef.current = null
58 }
59 }, [])
61 useEffect(() => {
62 setState(s => ({ ...s, computing: true }))
63 const id = ++idRef.current
64 const timer = setTimeout(() => {
65 const request: CompressRequest = {
66 id,
67 kernel,
68 sigma,
70 lpcOrder,
71 // The same seed the signal view draws from, so the block really is
72 // the data on screen.
73 seed: LATENT_SEED,
75 workerRef.current?.postMessage(request)
76 }, 250)
77 return () => clearTimeout(timer)
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 78 }, [kernel, sigma, lpcOrder, blockSize])
80 return state
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 84 * 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 85 * settings, using the unbiased Monte-Carlo estimator from the companion
86 * timeseries-entropy package.
87 */
88function mcCommand(sigma: number, spec: ReturnType<typeof clampSpec>, rate: number): string {
89 const parts = [
90 'uvx --from git+https://github.com/concept-collection/timeseries-entropy',
91 'timeseries-entropy',
92 `--sigma ${sigma}`,
93 ]
95 case 'none':
96 parts.push('--filter none')
97 break
98 case 'movingAverage':
99 parts.push('--filter moving-average', `--width ${spec.width}`)
100 break
101 case 'lowpass':
102 parts.push('--filter lowpass', `--high ${spec.highHz}`, `--taps ${spec.taps}`, `--rate ${rate}`)
103 break
104 case 'bandpass':
105 parts.push(
106 '--filter bandpass',
107 `--low ${spec.lowHz}`,
108 `--high ${spec.highHz}`,
109 `--taps ${spec.taps}`,
110 `--rate ${rate}`,
111 )
112 break
113 case 'firstDifference':
114 parts.push('--filter first-difference')
115 break
116 }
117 return parts.join(' ')
121 const [sigma, setSigma] = useState(5)
122 const [sampleRateHz, setSampleRateHz] = useState(30000)
123 const [spec, setSpec] = useState(DEFAULT_SPEC)
e411dffMake LPC order and compression block size controlsJeremy Magland 124 const [lpcOrder, setLpcOrder] = useState(DEFAULT_LPC_ORDER)
125 const [blockSize, setBlockSize] = useState(DEFAULT_BLOCK_SIZE)
127 const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 128 const sigmaY = useMemo(() => sigma * kernelNorm(kernel), [kernel, sigma])
129 const compression = useCompression(kernel, sigma, lpcOrder, blockSize)
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 130 const entropyRate = useEntropyRate(kernel, sigma)
132 return (
133 <div className="app">
134 <header className="app-header">
135 <h1>Time-series compressibility</h1>
136 <p>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 137 Gaussian noise → FIR filter → round to integers. How well can the integer stream be
138 losslessly compressed, and how close do practical codecs get to the entropy rate of
139 the process?
141 </header>
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 143 {/* The controls stay pinned so the parameters and the ratios they move
144 are always on screen together, whatever is scrolled to. */}
145 <section className="card control-bar">
147 sigma={sigma}
148 setSigma={setSigma}
149 sampleRateHz={sampleRateHz}
fb1a12fSnap every parameter slider to a ladder of round valuesJeremy Magland 150 setSampleRateHz={rate => {
151 // Band edges are absolute, so a new rate can push them past
152 // Nyquist; re-snap the spec so sliders and kernel stay in step.
153 setSampleRateHz(rate)
154 setSpec(s => clampSpec(s, rate))
155 }}
157 setSpec={setSpec}
158 />
159 </section>
161 <section className="card">
162 <h2>Compression</h2>
163 <div className="stat-row">
164 <div className="stat">
165 <span className="label">predicted std of z</span>
166 <span className="value">
167 {sigmaY.toFixed(2)} <small>steps</small>
168 </span>
169 </div>
170 <div className="stat">
171 <span className="label">measured std of z</span>
172 <span className="value">
173 {compression.results.length > 0 ? compression.empiricalStd.toFixed(2) : '…'}{' '}
174 <small>steps</small>
175 </span>
176 </div>
177 <div className="stat">
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 178 <span className="label">entropy rate R</span>
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 180 {entropyRate.mean !== null ? entropyRate.mean.toFixed(2) : '—'}
181 {entropyRate.se !== null && <small> ± {entropyRate.se.toFixed(2)}</small>}{' '}
3f857e3Estimate the reference rate R in the browserJeremy Magland 182 <small>bits/sample</small>
183 </span>
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 184 {/* The estimate lives with its readout: start, watch it refine,
185 stop; a model change resets it. */}
186 <span className="stat-action">
187 <button onClick={entropyRate.running ? entropyRate.stop : entropyRate.start}>
188 {entropyRate.running ? 'stop' : entropyRate.perPast.length > 0 ? 'refine' : 'estimate'}
189 </button>
190 {/* Each independent past contributes one unbiased estimate;
191 the readout is their average, so that is the word used. */}
192 <span className="estimate-status">
193 {entropyRate.running
194 ? `${entropyRate.perPast.length} estimates · ${entropyRate.progress ?? 'starting…'}`
195 : entropyRate.perPast.length > 0
196 ? `${entropyRate.perPast.length} estimates · M = ${entropyRate.past}`
197 : `M = ${entropyRate.past}`}
198 </span>
199 </span>
201 <div className="stat">
202 <span className="label">implied best ratio</span>
203 <span className="value">
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 204 {entropyRate.mean !== null && entropyRate.mean > 0
205 ? `${(16 / entropyRate.mean).toFixed(2)}×`
206 : '—'}
208 </div>
209 </div>
e411dffMake LPC order and compression block size controlsJeremy Magland 210 {/* Settings of the measurement, not of the model — so they live with
211 the chart they change rather than in the model bar. */}
212 <div className="measure-row">
213 <label>
214 LPC order
215 <select value={lpcOrder} onChange={e => setLpcOrder(Number(e.target.value))}>
216 {LPC_ORDERS.map(o => (
217 <option key={o} value={o}>
218 {o}
219 </option>
220 ))}
221 </select>
222 </label>
223 <label>
224 block size
225 <select value={blockSize} onChange={e => setBlockSize(Number(e.target.value))}>
226 {BLOCK_SIZES.map(n => (
227 <option key={n} value={n}>
228 {n.toLocaleString()} samples
229 </option>
230 ))}
231 </select>
232 </label>
233 </div>
235 <p className="card-note">Compression failed: {compression.error}</p>
236 ) : (
237 <CompressionChart
238 results={compression.results}
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 239 rateBits={entropyRate.mean}
241 />
242 )}
243 <p className="card-note">
e411dffMake LPC order and compression block size controlsJeremy Magland 244 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 245 signal view shows; sizes include everything a decoder needs (ANS symbol table, LPC
4ec0133Code against the prediction, not the integer residualJeremy Magland 246 coefficients). Baseline is raw int16 (16 bits/sample). The dashed line is the entropy
247 rate R of the process itself, estimated by the button in the stat row above — the one
248 limit no lossless method whatsoever can beat (see the method section at the bottom).
249 What separates the methods is the model each one codes against: ANS uses the histogram
250 of whatever stream it is given, so a better prefilter is the only way it improves,
251 while the conditional-Gaussian coder codes each sample against a prediction and can
252 therefore approach R. The strip under the chart scores each coder against its own
253 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 256 label="cross-check R from the command line:"
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 257 command={mcCommand(sigma, spec, sampleRateHz)}
262 <h2>Quantized signal z</h2>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 263 <ScrollingView kernel={kernel} sigma={sigma} sigmaY={sigmaY} />
265 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 266 σ or the filter transforms the same underlying data, so the trace morphs rather than
267 resampling. Press play to advance through the sequence.
269 </section>
271 <section className="card">
272 <h2>Filter</h2>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 273 <FilterViz kernel={kernel} sampleRateHz={sampleRateHz} />
280 </div>
281 )
↑↓ move↵openescclose