/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
timeseries-compressibility / src / App.tsx
279 lines · 10.1 KBBlameHistoryRaw
1import { useEffect, useMemo, useRef, useState } from 'react'
2import CopyableCommand from './components/CopyableCommand'
3import Controls from './components/Controls'
4import FilterViz from './components/FilterViz'
5import ScrollingView from './components/ScrollingView'
6import CompressionChart from './components/CompressionChart'
7import MethodNote from './components/MethodNote'
8import { useReferenceRate } from './components/useReferenceRate'
9import { DEFAULT_SPEC, clampSpec, designKernel, kernelNorm } from './model/filters'
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 lpcOrder: number,
31 blockSize: number,
32): CompressionState {
33 const [state, setState] = useState<CompressionState>({
34 results: [],
35 bounds: [],
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,
51 bounds: e.data.error ? [] : e.data.bounds,
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,
72 blockSize,
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,
77 }
78 workerRef.current?.postMessage(request)
79 }, 250)
80 return () => clearTimeout(timer)
81 }, [kernel, sigma, lpcOrder, blockSize])
83 return state
86/**
87 * The terminal command that estimates the reference rate R at the current
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 ]
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(' ')
123export default function App() {
124 const [sigma, setSigma] = useState(5)
125 const [sampleRateHz, setSampleRateHz] = useState(30000)
126 const [spec, setSpec] = useState(DEFAULT_SPEC)
127 const [lpcOrder, setLpcOrder] = useState(DEFAULT_LPC_ORDER)
128 const [blockSize, setBlockSize] = useState(DEFAULT_BLOCK_SIZE)
130 const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
131 const sigmaY = useMemo(() => sigma * kernelNorm(kernel), [kernel, sigma])
132 const compression = useCompression(kernel, sigma, lpcOrder, blockSize)
133 const refRate = useReferenceRate(kernel, sigma)
135 return (
136 <div className="app">
137 <header className="app-header">
138 <h1>Time-series compressibility</h1>
139 <p>
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?
143 </p>
144 </header>
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">
149 <Controls
150 sigma={sigma}
151 setSigma={setSigma}
152 sampleRateHz={sampleRateHz}
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 }}
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">
181 <span className="label">reference rate R</span>
182 <span className="value">
183 {refRate.mean !== null ? refRate.mean.toFixed(2) : '—'}
184 {refRate.se !== null && <small> ± {refRate.se.toFixed(2)}</small>}{' '}
185 <small>bits/sample</small>
186 </span>
187 </div>
188 <div className="stat">
189 <span className="label">implied best ratio</span>
190 <span className="value">
191 {refRate.mean !== null && refRate.mean > 0 ? `${(16 / refRate.mean).toFixed(2)}×` : '—'}
192 </span>
193 </div>
194 </div>
195 {/* Settings of the measurement, not of the model — so they live with
196 the chart they change rather than in the model bar. */}
197 <div className="measure-row">
198 <label>
199 LPC order
200 <select value={lpcOrder} onChange={e => setLpcOrder(Number(e.target.value))}>
201 {LPC_ORDERS.map(o => (
202 <option key={o} value={o}>
203 {o}
204 </option>
205 ))}
206 </select>
207 </label>
208 <label>
209 block size
210 <select value={blockSize} onChange={e => setBlockSize(Number(e.target.value))}>
211 {BLOCK_SIZES.map(n => (
212 <option key={n} value={n}>
213 {n.toLocaleString()} samples
214 </option>
215 ))}
216 </select>
217 </label>
218 </div>
219 {compression.error ? (
220 <p className="card-note">Compression failed: {compression.error}</p>
221 ) : (
222 <CompressionChart
223 results={compression.results}
224 bounds={compression.bounds}
225 refBits={refRate.mean}
226 computing={compression.computing}
227 />
228 )}
229 <p className="card-note">
230 Measured on a {blockSize.toLocaleString()}-sample block of the same latent data the
231 signal view shows; sizes include everything a decoder needs (ANS symbol table, LPC
232 coefficients). Baseline is raw int16 (16 bits/sample). The hollow bar in each group is
233 that group's entropy limit — the order-0 entropy of the stream being coded, which no
234 per-sample entropy coder can beat and ANS falls short of by its symbol table plus its
235 own arithmetic loss. The dashed line, once estimated, is the reference rate R — the
236 entropy rate of the process itself, the limit no lossless method whatsoever can beat
237 (see the method section at the bottom).
238 </p>
239 <div className="estimate-row">
240 <button onClick={refRate.running ? refRate.stop : refRate.start}>
241 {refRate.running ? 'stop' : refRate.perPast.length > 0 ? 'refine R further' : 'estimate R in this browser'}
242 </button>
243 <span className="estimate-status">
244 {refRate.running
245 ? `${refRate.perPast.length} independent pasts averaged, M = ${refRate.past}` +
246 (refRate.progress ? ` · ${refRate.progress}` : '')
247 : refRate.perPast.length > 0
248 ? `${refRate.perPast.length} independent pasts averaged, M = ${refRate.past}`
249 : `unbiased Monte-Carlo conditioning on M = ${refRate.past} past samples; refines until stopped`}
250 </span>
251 </div>
252 <CopyableCommand
253 label="or cross-check R from the command line:"
254 command={mcCommand(sigma, spec, sampleRateHz)}
255 />
256 </section>
258 <section className="card">
259 <h2>Quantized signal z</h2>
260 <ScrollingView kernel={kernel} sigma={sigma} sigmaY={sigmaY} />
261 <p className="card-note">
262 A window of samples from the model, drawn from a fixed latent noise sequence — changing
263 σ or the filter transforms the same underlying data, so the trace morphs rather than
264 resampling. Press play to advance through the sequence.
265 </p>
266 </section>
268 <section className="card">
269 <h2>Filter</h2>
270 <FilterViz kernel={kernel} sampleRateHz={sampleRateHz} />
271 </section>
273 <section className="card">
274 <h2>The reference rate</h2>
275 <MethodNote />
276 </section>
277 </div>
278 )
moveopenescclose