import { useEffect, useMemo, useRef, useState } from 'react' import CopyableCommand from './components/CopyableCommand' import Controls from './components/Controls' import FilterViz from './components/FilterViz' import ScrollingView from './components/ScrollingView' import CompressionChart from './components/CompressionChart' import MethodNote from './components/MethodNote' import { useReferenceRate } from './components/useReferenceRate' import { DEFAULT_SPEC, clampSpec, designKernel, kernelNorm } from './model/filters' import { LATENT_SEED } from './model/latent' import { DEFAULT_LPC_ORDER, LPC_ORDERS } from './compress/codecs' import type { BoundResult, CodecResult } from './compress/codecs' import type { CompressRequest, CompressResponse } from './worker/compressWorker' const BLOCK_SIZES = [10000, 20000, 50000, 100000, 200000, 500000, 1000000] const DEFAULT_BLOCK_SIZE = 100000 interface CompressionState { results: CodecResult[] bounds: BoundResult[] empiricalStd: number computing: boolean error: string | null } /** The nine codec sizes, measured in a worker on a debounced parameter set. */ function useCompression( kernel: Float64Array, sigma: number, lpcOrder: number, blockSize: number, ): CompressionState { const [state, setState] = useState({ results: [], bounds: [], empiricalStd: 0, computing: true, error: null, }) const workerRef = useRef(null) const idRef = useRef(0) useEffect(() => { const worker = new Worker(new URL('./worker/compressWorker.ts', import.meta.url), { type: 'module', }) worker.onmessage = (e: MessageEvent) => { if (e.data.id !== idRef.current) return setState({ results: e.data.error ? [] : e.data.results, bounds: e.data.error ? [] : e.data.bounds, empiricalStd: e.data.empiricalStd, computing: false, error: e.data.error ?? null, }) } workerRef.current = worker return () => { worker.terminate() workerRef.current = null } }, []) useEffect(() => { setState(s => ({ ...s, computing: true })) const id = ++idRef.current const timer = setTimeout(() => { const request: CompressRequest = { id, kernel, sigma, blockSize, lpcOrder, // The same seed the signal view draws from, so the block really is // the data on screen. seed: LATENT_SEED, } workerRef.current?.postMessage(request) }, 250) return () => clearTimeout(timer) }, [kernel, sigma, lpcOrder, blockSize]) return state } /** * The terminal command that estimates the reference rate R at the current * settings, using the unbiased Monte-Carlo estimator from the companion * timeseries-entropy package. */ function mcCommand(sigma: number, spec: ReturnType, rate: number): string { const parts = [ 'uvx --from git+https://github.com/concept-collection/timeseries-entropy', 'timeseries-entropy', `--sigma ${sigma}`, ] switch (spec.family) { case 'none': parts.push('--filter none') break case 'movingAverage': parts.push('--filter moving-average', `--width ${spec.width}`) break case 'lowpass': parts.push('--filter lowpass', `--high ${spec.highHz}`, `--taps ${spec.taps}`, `--rate ${rate}`) break case 'bandpass': parts.push( '--filter bandpass', `--low ${spec.lowHz}`, `--high ${spec.highHz}`, `--taps ${spec.taps}`, `--rate ${rate}`, ) break case 'firstDifference': parts.push('--filter first-difference') break } return parts.join(' ') } export default function App() { const [sigma, setSigma] = useState(5) const [sampleRateHz, setSampleRateHz] = useState(30000) const [spec, setSpec] = useState(DEFAULT_SPEC) const [lpcOrder, setLpcOrder] = useState(DEFAULT_LPC_ORDER) const [blockSize, setBlockSize] = useState(DEFAULT_BLOCK_SIZE) const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz]) const sigmaY = useMemo(() => sigma * kernelNorm(kernel), [kernel, sigma]) const compression = useCompression(kernel, sigma, lpcOrder, blockSize) const refRate = useReferenceRate(kernel, sigma) return (

Time-series compressibility

Gaussian noise → FIR filter → round to integers. How well can the integer stream be losslessly compressed, and how close do practical codecs get to the entropy rate of the process?

{/* The controls stay pinned so the parameters and the ratios they move are always on screen together, whatever is scrolled to. */}
{ // Band edges are absolute, so a new rate can push them past // Nyquist; re-snap the spec so sliders and kernel stay in step. setSampleRateHz(rate) setSpec(s => clampSpec(s, rate)) }} spec={spec} setSpec={setSpec} />

Compression

predicted std of z {sigmaY.toFixed(2)} steps
measured std of z {compression.results.length > 0 ? compression.empiricalStd.toFixed(2) : '…'}{' '} steps
reference rate R {refRate.mean !== null ? refRate.mean.toFixed(2) : '—'} {refRate.se !== null && ± {refRate.se.toFixed(2)}}{' '} bits/sample
implied best ratio {refRate.mean !== null && refRate.mean > 0 ? `${(16 / refRate.mean).toFixed(2)}×` : '—'}
{/* Settings of the measurement, not of the model — so they live with the chart they change rather than in the model bar. */}
{compression.error ? (

Compression failed: {compression.error}

) : ( )}

Measured on a {blockSize.toLocaleString()}-sample block of the same latent data the signal view shows; sizes include everything a decoder needs (ANS symbol table, LPC coefficients). Baseline is raw int16 (16 bits/sample). The hollow bar in each group is that group's entropy limit — the order-0 entropy of the stream being coded, which no per-sample entropy coder can beat and ANS falls short of by its symbol table plus its own arithmetic loss. The dashed line, once estimated, is the reference rate R — the entropy rate of the process itself, the limit no lossless method whatsoever can beat (see the method section at the bottom).

{refRate.running ? `${refRate.perPast.length} independent pasts averaged, M = ${refRate.past}` + (refRate.progress ? ` · ${refRate.progress}` : '') : refRate.perPast.length > 0 ? `${refRate.perPast.length} independent pasts averaged, M = ${refRate.past}` : `unbiased Monte-Carlo conditioning on M = ${refRate.past} past samples; refines until stopped`}

Quantized signal z

A window of samples from the model, drawn from a fixed latent noise sequence — changing σ or the filter transforms the same underlying data, so the trace morphs rather than resampling. Press play to advance through the sequence.

Filter

The reference rate

) }