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'
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
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 17
18interface CompressionState {
19 results: CodecResult[]
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 20 bounds: BoundResult[]
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 21 empiricalStd: number
22 computing: boolean
23 error: string | null
24}
26/** The nine codec sizes, measured in a worker on a debounced parameter set. */
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: [],
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 36 empiricalStd: 0,
37 computing: true,
38 error: null,
39 })
40 const workerRef = useRef<Worker | null>(null)
41 const idRef = useRef(0)
43 useEffect(() => {
44 const worker = new Worker(new URL('./worker/compressWorker.ts', import.meta.url), {
45 type: 'module',
46 })
47 worker.onmessage = (e: MessageEvent<CompressResponse>) => {
48 if (e.data.id !== idRef.current) return
49 setState({
50 results: e.data.error ? [] : e.data.results,
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 51 bounds: e.data.error ? [] : e.data.bounds,
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 52 empiricalStd: e.data.empiricalStd,
53 computing: false,
54 error: e.data.error ?? null,
55 })
56 }
57 workerRef.current = worker
58 return () => {
59 worker.terminate()
60 workerRef.current = null
61 }
62 }, [])
64 useEffect(() => {
65 setState(s => ({ ...s, computing: true }))
66 const id = ++idRef.current
67 const timer = setTimeout(() => {
68 const request: CompressRequest = {
69 id,
70 kernel,
71 sigma,
73 lpcOrder,
74 // The same seed the signal view draws from, so the block really is
75 // the data on screen.
76 seed: LATENT_SEED,
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 77 }
78 workerRef.current?.postMessage(request)
79 }, 250)
80 return () => clearTimeout(timer)
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 81 }, [kernel, sigma, lpcOrder, blockSize])
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 82
83 return state
84}
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 86/**
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 87 * 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 88 * settings, using the unbiased Monte-Carlo estimator from the companion
89 * timeseries-entropy package.
90 */
91function mcCommand(sigma: number, spec: ReturnType<typeof clampSpec>, rate: number): string {
92 const parts = [
93 'uvx --from git+https://github.com/concept-collection/timeseries-entropy',
94 'timeseries-entropy',
95 `--sigma ${sigma}`,
96 ]
95929e6Monte-Carlo ground-truth script for R, with the command printed in the UIJeremy Magland 97 switch (spec.family) {
98 case 'none':
99 parts.push('--filter none')
100 break
101 case 'movingAverage':
102 parts.push('--filter moving-average', `--width ${spec.width}`)
103 break
104 case 'lowpass':
105 parts.push('--filter lowpass', `--high ${spec.highHz}`, `--taps ${spec.taps}`, `--rate ${rate}`)
106 break
107 case 'bandpass':
108 parts.push(
109 '--filter bandpass',
110 `--low ${spec.lowHz}`,
111 `--high ${spec.highHz}`,
112 `--taps ${spec.taps}`,
113 `--rate ${rate}`,
114 )
115 break
116 case 'firstDifference':
117 parts.push('--filter first-difference')
118 break
119 }
120 return parts.join(' ')
121}
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 123export default function App() {
124 const [sigma, setSigma] = useState(5)
125 const [sampleRateHz, setSampleRateHz] = useState(30000)
126 const [spec, setSpec] = useState(DEFAULT_SPEC)
e411dffMake LPC order and compression block size controlsJeremy Magland 127 const [lpcOrder, setLpcOrder] = useState(DEFAULT_LPC_ORDER)
128 const [blockSize, setBlockSize] = useState(DEFAULT_BLOCK_SIZE)
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 129
130 const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 131 const sigmaY = useMemo(() => sigma * kernelNorm(kernel), [kernel, sigma])
132 const compression = useCompression(kernel, sigma, lpcOrder, blockSize)
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 133 const entropyRate = useEntropyRate(kernel, sigma)
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 134
135 return (
136 <div className="app">
137 <header className="app-header">
138 <h1>Time-series compressibility</h1>
139 <p>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 140 Gaussian noise → FIR filter → round to integers. How well can the integer stream be
141 losslessly compressed, and how close do practical codecs get to the entropy rate of
142 the process?
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 143 </p>
144 </header>
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 146 {/* The controls stay pinned so the parameters and the ratios they move
147 are always on screen together, whatever is scrolled to. */}
148 <section className="card control-bar">
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 149 <Controls
150 sigma={sigma}
151 setSigma={setSigma}
152 sampleRateHz={sampleRateHz}
fb1a12fSnap every parameter slider to a ladder of round valuesJeremy Magland 153 setSampleRateHz={rate => {
154 // Band edges are absolute, so a new rate can push them past
155 // Nyquist; re-snap the spec so sliders and kernel stay in step.
156 setSampleRateHz(rate)
157 setSpec(s => clampSpec(s, rate))
158 }}
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 159 spec={spec}
160 setSpec={setSpec}
161 />
162 </section>
164 <section className="card">
165 <h2>Compression</h2>
166 <div className="stat-row">
167 <div className="stat">
168 <span className="label">predicted std of z</span>
169 <span className="value">
170 {sigmaY.toFixed(2)} <small>steps</small>
171 </span>
172 </div>
173 <div className="stat">
174 <span className="label">measured std of z</span>
175 <span className="value">
176 {compression.results.length > 0 ? compression.empiricalStd.toFixed(2) : '…'}{' '}
177 <small>steps</small>
178 </span>
179 </div>
180 <div className="stat">
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 181 <span className="label">entropy rate R</span>
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 182 <span className="value">
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 183 {entropyRate.mean !== null ? entropyRate.mean.toFixed(2) : '—'}
184 {entropyRate.se !== null && <small> ± {entropyRate.se.toFixed(2)}</small>}{' '}
186 </span>
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 187 {/* The estimate lives with its readout: start, watch it refine,
188 stop; a model change resets it. */}
189 <span className="stat-action">
190 <button onClick={entropyRate.running ? entropyRate.stop : entropyRate.start}>
191 {entropyRate.running ? 'stop' : entropyRate.perPast.length > 0 ? 'refine' : 'estimate'}
192 </button>
193 {/* Each independent past contributes one unbiased estimate;
194 the readout is their average, so that is the word used. */}
195 <span className="estimate-status">
196 {entropyRate.running
197 ? `${entropyRate.perPast.length} estimates · ${entropyRate.progress ?? 'starting…'}`
198 : entropyRate.perPast.length > 0
199 ? `${entropyRate.perPast.length} estimates · M = ${entropyRate.past}`
200 : `M = ${entropyRate.past}`}
201 </span>
202 </span>
204 <div className="stat">
205 <span className="label">implied best ratio</span>
206 <span className="value">
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 207 {entropyRate.mean !== null && entropyRate.mean > 0
208 ? `${(16 / entropyRate.mean).toFixed(2)}×`
209 : '—'}
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 210 </span>
211 </div>
212 </div>
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>
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 237 {compression.error ? (
238 <p className="card-note">Compression failed: {compression.error}</p>
239 ) : (
240 <CompressionChart
241 results={compression.results}
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 242 bounds={compression.bounds}
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 243 rateBits={entropyRate.mean}
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 244 computing={compression.computing}
245 />
246 )}
247 <p className="card-note">
e411dffMake LPC order and compression block size controlsJeremy Magland 248 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 249 signal view shows; sizes include everything a decoder needs (ANS symbol table, LPC
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 250 coefficients). Baseline is raw int16 (16 bits/sample). The hollow bar in each group is
251 that group's entropy limit — the order-0 entropy of the stream being coded, which no
252 per-sample entropy coder can beat and ANS falls short of by its symbol table plus its
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 253 own arithmetic loss. The dashed line is the entropy rate R of the process itself,
254 estimated by the button in the stat row above — the limit no lossless method
255 whatsoever can beat (see the method section at the bottom).
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 256 </p>
95929e6Monte-Carlo ground-truth script for R, with the command printed in the UIJeremy Magland 257 <CopyableCommand
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 258 label="cross-check R from the command line:"
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 259 command={mcCommand(sigma, spec, sampleRateHz)}
95929e6Monte-Carlo ground-truth script for R, with the command printed in the UIJeremy Magland 260 />
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 261 </section>
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 263 <section className="card">
264 <h2>Quantized signal z</h2>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 265 <ScrollingView kernel={kernel} sigma={sigma} sigmaY={sigmaY} />
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 266 <p className="card-note">
267 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 268 σ or the filter transforms the same underlying data, so the trace morphs rather than
269 resampling. Press play to advance through the sequence.
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 270 </p>
271 </section>
273 <section className="card">
274 <h2>Filter</h2>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 275 <FilterViz kernel={kernel} sampleRateHz={sampleRateHz} />
8a09dfcPin the parameter bar, put compression first, shrink signal and filter panelsJeremy Magland 276 </section>
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 278 <section className="card">
6b485dfCall R the entropy rate, and put the estimate button under its readoutJeremy Magland 279 <h2>The entropy rate</h2>
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 280 <MethodNote />
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 281 </section>
282 </div>
283 )
284}