/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
timeseries-compressibility / src / App.tsx
178 lines · 6.1 KBBlameHistoryRaw
1import { useEffect, useMemo, useRef, useState } from 'react'
2import Controls from './components/Controls'
3import FilterViz from './components/FilterViz'
4import ScrollingView from './components/ScrollingView'
5import CompressionChart from './components/CompressionChart'
6import MathSection from './components/MathSection'
7import { DEFAULT_SPEC, designKernel, kernelNorm } from './model/filters'
8import { theoreticalRateBits } from './model/theory'
9import type { CodecResult } from './compress/codecs'
10import type { CompressRequest, CompressResponse } from './worker/compressWorker'
12const BLOCK_SIZE = 120000
13const BLOCK_SEED = 20260729
15interface CompressionState {
16 results: CodecResult[]
17 empiricalStd: number
18 computing: boolean
19 error: string | null
22/** The nine codec sizes, measured in a worker on a debounced parameter set. */
23function useCompression(kernel: Float64Array, sigma: number, dither: boolean): CompressionState {
24 const [state, setState] = useState<CompressionState>({
25 results: [],
26 empiricalStd: 0,
27 computing: true,
28 error: null,
29 })
30 const workerRef = useRef<Worker | null>(null)
31 const idRef = useRef(0)
33 useEffect(() => {
34 const worker = new Worker(new URL('./worker/compressWorker.ts', import.meta.url), {
35 type: 'module',
36 })
37 worker.onmessage = (e: MessageEvent<CompressResponse>) => {
38 if (e.data.id !== idRef.current) return
39 setState({
40 results: e.data.error ? [] : e.data.results,
41 empiricalStd: e.data.empiricalStd,
42 computing: false,
43 error: e.data.error ?? null,
44 })
45 }
46 workerRef.current = worker
47 return () => {
48 worker.terminate()
49 workerRef.current = null
50 }
51 }, [])
53 useEffect(() => {
54 setState(s => ({ ...s, computing: true }))
55 const id = ++idRef.current
56 const timer = setTimeout(() => {
57 const request: CompressRequest = {
58 id,
59 kernel,
60 sigma,
61 dither,
62 blockSize: BLOCK_SIZE,
63 seed: BLOCK_SEED,
64 }
65 workerRef.current?.postMessage(request)
66 }, 250)
67 return () => clearTimeout(timer)
68 }, [kernel, sigma, dither])
70 return state
73export default function App() {
74 const [sigma, setSigma] = useState(5)
75 const [sampleRateHz, setSampleRateHz] = useState(30000)
76 const [spec, setSpec] = useState(DEFAULT_SPEC)
77 const [dither, setDither] = useState(false)
79 const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
80 const sigmaY = useMemo(() => {
81 const filtered = sigma * kernelNorm(kernel)
82 return dither ? Math.sqrt(filtered * filtered + 1 / 12) : filtered
83 }, [kernel, sigma, dither])
84 const theoryBits = useMemo(() => theoreticalRateBits(kernel, sigma, dither), [kernel, sigma, dither])
85 const compression = useCompression(kernel, sigma, dither)
87 return (
88 <div className="app">
89 <header className="app-header">
90 <h1>Time-series compressibility</h1>
91 <p>
92 Gaussian noise → FIR filter → optional dither → round to integers. How well can the
93 integer stream be losslessly compressed, and does the spectral entropy-rate formula
94 predict the limit?
95 </p>
96 </header>
98 <section className="card">
99 <h2>Model</h2>
100 <Controls
101 sigma={sigma}
102 setSigma={setSigma}
103 sampleRateHz={sampleRateHz}
104 setSampleRateHz={setSampleRateHz}
105 spec={spec}
106 setSpec={setSpec}
107 dither={dither}
108 setDither={setDither}
109 />
110 </section>
112 <section className="card">
113 <h2>Filter</h2>
114 <FilterViz kernel={kernel} sampleRateHz={sampleRateHz} sigma={sigma} />
115 </section>
117 <section className="card">
118 <h2>Quantized signal z</h2>
119 <ScrollingView kernel={kernel} sigma={sigma} dither={dither} sigmaY={sigmaY} />
120 <p className="card-note">
121 A window of samples from the model, drawn from a fixed latent noise sequence — changing
122 σ, the filter, or dither transforms the same underlying data, so the trace morphs
123 rather than resampling. Press play to advance through the sequence.
124 </p>
125 </section>
127 <section className="card">
128 <h2>Compression</h2>
129 <div className="stat-row">
130 <div className="stat">
131 <span className="label">predicted std of z</span>
132 <span className="value">
133 {sigmaY.toFixed(2)} <small>steps</small>
134 </span>
135 </div>
136 <div className="stat">
137 <span className="label">measured std of z</span>
138 <span className="value">
139 {compression.results.length > 0 ? compression.empiricalStd.toFixed(2) : '…'}{' '}
140 <small>steps</small>
141 </span>
142 </div>
143 <div className="stat">
144 <span className="label">theoretical rate R</span>
145 <span className="value">
146 {theoryBits.toFixed(2)} <small>bits/sample</small>
147 </span>
148 </div>
149 <div className="stat">
150 <span className="label">implied best ratio</span>
151 <span className="value">{theoryBits > 0 ? `${(16 / theoryBits).toFixed(2)}×` : '—'}</span>
152 </div>
153 </div>
154 {compression.error ? (
155 <p className="card-note">Compression failed: {compression.error}</p>
156 ) : (
157 <CompressionChart
158 results={compression.results}
159 theoryBits={theoryBits}
160 computing={compression.computing}
161 />
162 )}
163 <p className="card-note">
164 Measured on a {BLOCK_SIZE.toLocaleString()}-sample block of the same latent data the
165 signal view shows; sizes include everything a decoder needs (ANS symbol table, LPC
166 coefficients). Baseline is raw int16 (16 bits/sample). The dashed line is the
167 theoretical rate R from the spectral formula in the math section — approximate where
168 quantization dominates the spectrum (see the S(f) = 1 threshold on the response plot).
169 </p>
170 </section>
172 <section className="card">
173 <h2>The math</h2>
174 <MathSection />
175 </section>
176 </div>
177 )
moveopenescclose