/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
timeseries-compressibility / src / App.tsx
177 lines · 5.9 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 { entropyRateBits } 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(() => entropyRateBits(kernel, sigma), [kernel, sigma])
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 Continuously generated from the model; sample-and-hold rendering, so the integer
122 staircase appears as σ approaches the quantization step.
123 </p>
124 </section>
126 <section className="card">
127 <h2>Compression</h2>
128 <div className="stat-row">
129 <div className="stat">
130 <span className="label">predicted std of z</span>
131 <span className="value">
132 {sigmaY.toFixed(2)} <small>steps</small>
133 </span>
134 </div>
135 <div className="stat">
136 <span className="label">measured std of z</span>
137 <span className="value">
138 {compression.results.length > 0 ? compression.empiricalStd.toFixed(2) : '…'}{' '}
139 <small>steps</small>
140 </span>
141 </div>
142 <div className="stat">
143 <span className="label">entropy rate R (theory)</span>
144 <span className="value">
145 {theoryBits.toFixed(2)} <small>bits/sample</small>
146 </span>
147 </div>
148 <div className="stat">
149 <span className="label">implied best ratio</span>
150 <span className="value">{theoryBits > 0 ? `${(16 / theoryBits).toFixed(2)}×` : '—'}</span>
151 </div>
152 </div>
153 {compression.error ? (
154 <p className="card-note">Compression failed: {compression.error}</p>
155 ) : (
156 <CompressionChart
157 results={compression.results}
158 theoryBits={theoryBits}
159 computing={compression.computing}
160 />
161 )}
162 <p className="card-note">
163 Measured on a {BLOCK_SIZE.toLocaleString()}-sample block from the same model; sizes
164 include everything a decoder needs (ANS symbol table, LPC coefficients). Baseline is
165 raw int16 (16 bits/sample). The dashed line is the high-resolution entropy rate R — it
166 ignores dither and is unreliable where S(f) falls below one step² (see the response
167 plot).
168 </p>
169 </section>
171 <section className="card">
172 <h2>The math</h2>
173 <MathSection />
174 </section>
175 </div>
176 )
moveopenescclose