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
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 17
18interface CompressionState {
19 results: CodecResult[]
20 empiricalStd: number
21 computing: boolean
22 error: string | null
23}
4ec0133Code against the prediction, not the integer residualJeremy Magland 25/** The ten codec sizes, measured in a worker on a debounced parameter set. */
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,
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 74 }
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])
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 79
80 return state
81}
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 83/**
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 ]
95929e6Monte-Carlo ground-truth script for R, with the command printed in the UIJeremy Magland 94 switch (spec.family) {
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(' ')
118}
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 120export default function App() {
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)
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 126
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)
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 131
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?
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 140 </p>
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">
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 146 <Controls
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 }}
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 156 spec={spec}
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>
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 179 <span className="value">
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>}{' '}
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 : '—'}
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 207 </span>
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>
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 234 {compression.error ? (
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}
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 240 computing={compression.computing}
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.
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 254 </p>
95929e6Monte-Carlo ground-truth script for R, with the command printed in the UIJeremy Magland 255 <CopyableCommand
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)}
95929e6Monte-Carlo ground-truth script for R, with the command printed in the UIJeremy Magland 258 />
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 259 </section>
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 261 <section className="card">
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} />
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 264 <p className="card-note">
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.
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 268 </p>
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} />
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 274 </section>
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 276 <section className="card">
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 277 <h2>The entropy rate</h2>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 278 <MethodNote />
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 279 </section>
280 </div>
281 )
282}