Call R the entropy rate, and put the estimate button under its readout
'Reference rate' was vague and 'pasts' was jargon: the stat is now
'entropy rate R' (matching the header's question), the button lives in
the stat itself (estimate / stop / refine), and the status counts
'estimates' - one unbiased value per independent past, averaged into
the readout. Code vocabulary follows (useEntropyRate, rateBits).
8 changed files+99−94
README.mdmodified+4−5View file
@@ -16,9 +16,8 @@ bar: the order-0 entropy of the stream being coded, the limit a per-sample
1616 entropy coder cannot beat, which ANS misses by 1–2% (its symbol table plus its
1717 own arithmetic loss).
1818
19-The reference rate R — the entropy rate of the process, the bits/sample limit
20-no lossless method can beat — is estimated in the browser by the method of the
21-companion
19+The entropy rate R of the process — the bits/sample limit no lossless method
20+can beat — is estimated in the browser by the method of the companion
2221 [timeseries-entropy](https://github.com/concept-collection/timeseries-entropy)
2322 package: an unbiased Monte-Carlo estimator of H(z_next | a long past), by Gibbs
2423 sampling the latent Gaussian under the rounding constraints and applying
@@ -43,7 +42,7 @@ npm run dev
4342 src/model/ the latent source (fixed seeded randomness indexed by sample
4443 position, convolved zero-phase with the kernel on demand)
4544 and the FIR presets
46-src/entropy/ the unbiased reference-rate estimator: hand-synced TypeScript
45+src/entropy/ the unbiased entropy-rate estimator: hand-synced TypeScript
4746 port of the timeseries-entropy package (Gibbs conditional
4847 sampler, Rhee–Glynn telescoping, Cody erfc / Acklam ndtri,
4948 xoshiro128** RNG)
@@ -53,7 +52,7 @@ src/compress/ lossless codecs run in the browser: zlib (fflate), zstd (wasm),
5352 src/worker/ the codecs and the estimator run off the main thread; the
5453 estimator worker refines one past at a time until terminated
5554 src/components/ controls, filter plots, signal canvas, compression chart,
56- and the reference-rate method note
55+ and the entropy-rate method note
5756 ```
5857
5958 Every reported size round-trips through the decoder and includes whatever the
src/App.tsxmodified+31−26View file
@@ -5,7 +5,7 @@ import FilterViz from './components/FilterViz'
55 import ScrollingView from './components/ScrollingView'
66 import CompressionChart from './components/CompressionChart'
77 import MethodNote from './components/MethodNote'
8-import { useReferenceRate } from './components/useReferenceRate'
8+import { useEntropyRate } from './components/useEntropyRate'
99 import { DEFAULT_SPEC, clampSpec, designKernel, kernelNorm } from './model/filters'
1010 import { LATENT_SEED } from './model/latent'
1111 import { DEFAULT_LPC_ORDER, LPC_ORDERS } from './compress/codecs'
@@ -84,7 +84,7 @@ function useCompression(
8484 }
8585
8686 /**
87- * The terminal command that estimates the reference rate R at the current
87+ * The terminal command that estimates the entropy rate R at the current
8888 * settings, using the unbiased Monte-Carlo estimator from the companion
8989 * timeseries-entropy package.
9090 */
@@ -130,7 +130,7 @@ export default function App() {
130130 const kernel = useMemo(() => designKernel(spec, sampleRateHz), [spec, sampleRateHz])
131131 const sigmaY = useMemo(() => sigma * kernelNorm(kernel), [kernel, sigma])
132132 const compression = useCompression(kernel, sigma, lpcOrder, blockSize)
133- const refRate = useReferenceRate(kernel, sigma)
133+ const entropyRate = useEntropyRate(kernel, sigma)
134134
135135 return (
136136 <div className="app">
@@ -178,17 +178,35 @@ export default function App() {
178178 </span>
179179 </div>
180180 <div className="stat">
181- <span className="label">reference rate R</span>
181+ <span className="label">entropy rate R</span>
182182 <span className="value">
183- {refRate.mean !== null ? refRate.mean.toFixed(2) : '—'}
184- {refRate.se !== null && <small> ± {refRate.se.toFixed(2)}</small>}{' '}
183+ {entropyRate.mean !== null ? entropyRate.mean.toFixed(2) : '—'}
184+ {entropyRate.se !== null && <small> ± {entropyRate.se.toFixed(2)}</small>}{' '}
185185 <small>bits/sample</small>
186186 </span>
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>
187203 </div>
188204 <div className="stat">
189205 <span className="label">implied best ratio</span>
190206 <span className="value">
191- {refRate.mean !== null && refRate.mean > 0 ? `${(16 / refRate.mean).toFixed(2)}×` : '—'}
207+ {entropyRate.mean !== null && entropyRate.mean > 0
208+ ? `${(16 / entropyRate.mean).toFixed(2)}×`
209+ : '—'}
192210 </span>
193211 </div>
194212 </div>
@@ -222,7 +240,7 @@ export default function App() {
222240 <CompressionChart
223241 results={compression.results}
224242 bounds={compression.bounds}
225- refBits={refRate.mean}
243+ rateBits={entropyRate.mean}
226244 computing={compression.computing}
227245 />
228246 )}
@@ -232,25 +250,12 @@ export default function App() {
232250 coefficients). Baseline is raw int16 (16 bits/sample). The hollow bar in each group is
233251 that group's entropy limit — the order-0 entropy of the stream being coded, which no
234252 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).
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).
238256 </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>
252257 <CopyableCommand
253- label="or cross-check R from the command line:"
258+ label="cross-check R from the command line:"
254259 command={mcCommand(sigma, spec, sampleRateHz)}
255260 />
256261 </section>
@@ -271,7 +276,7 @@ export default function App() {
271276 </section>
272277
273278 <section className="card">
274- <h2>The reference rate</h2>
279+ <h2>The entropy rate</h2>
275280 <MethodNote />
276281 </section>
277282 </div>
src/app.cssmodified+30−29View file
@@ -276,6 +276,36 @@ body {
276276 color: var(--muted);
277277 }
278278
279+.stat-action {
280+ display: flex;
281+ align-items: center;
282+ gap: 8px;
283+ margin-top: 5px;
284+ font-size: 12px;
285+}
286+
287+.stat-action button {
288+ background: var(--surface);
289+ color: var(--ink);
290+ border: 1px solid var(--baseline);
291+ border-radius: 6px;
292+ padding: 1px 10px;
293+ font: inherit;
294+ font-size: 12px;
295+ font-weight: 600;
296+ cursor: pointer;
297+}
298+
299+.stat-action button:hover {
300+ border-color: var(--series-1);
301+}
302+
303+.estimate-status {
304+ color: var(--muted);
305+ font-variant-numeric: tabular-nums;
306+ white-space: nowrap;
307+}
308+
279309 .measure-row {
280310 display: flex;
281311 flex-wrap: wrap;
@@ -443,35 +473,6 @@ body {
443473 font-weight: 500;
444474 }
445475
446-.estimate-row {
447- display: flex;
448- flex-wrap: wrap;
449- align-items: center;
450- gap: 6px 10px;
451- margin-top: 12px;
452- font-size: 12px;
453-}
454-
455-.estimate-row button {
456- background: var(--surface);
457- color: var(--ink);
458- border: 1px solid var(--baseline);
459- border-radius: 6px;
460- padding: 4px 12px;
461- font: inherit;
462- font-weight: 600;
463- cursor: pointer;
464-}
465-
466-.estimate-row button:hover {
467- border-color: var(--series-1);
468-}
469-
470-.estimate-status {
471- color: var(--muted);
472- font-variant-numeric: tabular-nums;
473-}
474-
475476 .command-row {
476477 display: flex;
477478 flex-wrap: wrap;
src/components/CompressionChart.tsxmodified+22−22View file
@@ -89,8 +89,8 @@ function buildRows(results: CodecResult[], bounds: BoundResult[]): Row[] {
8989 export default function CompressionChart(props: {
9090 results: CodecResult[]
9191 bounds: BoundResult[]
92- /** The browser-estimated reference rate R, once at least one past is in. */
93- refBits: number | null
92+ /** The browser-estimated entropy rate R, once at least one past is in. */
93+ rateBits: number | null
9494 computing: boolean
9595 }) {
9696 const ref = useRef<HTMLDivElement>(null)
@@ -99,7 +99,7 @@ export default function CompressionChart(props: {
9999 const [tip, setTip] = useState<Tip | null>(null)
100100 const [hovered, setHovered] = useState<string | null>(null)
101101
102- const { results, bounds, refBits } = props
102+ const { results, bounds, rateBits } = props
103103 if (results.length === 0) {
104104 return <p className="card-note">Computing compression on the first block…</p>
105105 }
@@ -107,12 +107,12 @@ export default function CompressionChart(props: {
107107 const rows = buildRows(results, bounds)
108108 const value = (r: { bitsPerSample: number; ratio: number }) =>
109109 metric === 'bits' ? r.bitsPerSample : r.ratio
110- const refValue =
111- refBits !== null && refBits > 0 ? (metric === 'bits' ? refBits : 16 / refBits) : null
110+ const rateValue =
111+ rateBits !== null && rateBits > 0 ? (metric === 'bits' ? rateBits : 16 / rateBits) : null
112112 const xMax =
113113 metric === 'bits'
114- ? Math.max(16, ...rows.map(value), refValue ?? 0) * 1.02
115- : Math.max(...rows.map(value), refValue ?? 0) * 1.1
114+ ? Math.max(16, ...rows.map(value), rateValue ?? 0) * 1.02
115+ : Math.max(...rows.map(value), rateValue ?? 0) * 1.1
116116
117117 const plotW = width - LABEL_W - RIGHT_PAD
118118 const height = AXIS_H + GROUPS.length * (GROUP_H + ROWS_PER_GROUP * ROW_H) + 6
@@ -130,12 +130,12 @@ export default function CompressionChart(props: {
130130 setTip({ x: e.clientX - box.left, y: e.clientY - box.top, row })
131131 }
132132
133- const refX = refValue !== null ? xOf(refValue) : 0
134- const refLabel =
135- refBits !== null && refBits > 0
133+ const rateX = rateValue !== null ? xOf(rateValue) : 0
134+ const rateLabel =
135+ rateBits !== null && rateBits > 0
136136 ? metric === 'bits'
137- ? `R = ${refBits.toFixed(2)}`
138- : `R ⇒ ${(16 / refBits).toFixed(2)}×`
137+ ? `R = ${rateBits.toFixed(2)}`
138+ : `R ⇒ ${(16 / rateBits).toFixed(2)}×`
139139 : ''
140140
141141 return (
@@ -238,11 +238,11 @@ export default function CompressionChart(props: {
238238 </g>
239239 )
240240 })}
241- {refValue !== null && (
241+ {rateValue !== null && (
242242 <g>
243243 <line
244- x1={refX}
245- x2={refX}
244+ x1={rateX}
245+ x2={rateX}
246246 y1={AXIS_H - 2}
247247 y2={height - 4}
248248 stroke="var(--ink-2)"
@@ -250,13 +250,13 @@ export default function CompressionChart(props: {
250250 strokeDasharray="5 4"
251251 />
252252 <text
253- x={refX + (refX > width - 150 ? -6 : 6)}
253+ x={rateX + (rateX > width - 150 ? -6 : 6)}
254254 y={AXIS_H + 10}
255- textAnchor={refX > width - 150 ? 'end' : 'start'}
255+ textAnchor={rateX > width - 150 ? 'end' : 'start'}
256256 className="bar-value"
257257 fill="var(--ink)"
258258 >
259- {refLabel}
259+ {rateLabel}
260260 </text>
261261 </g>
262262 )}
@@ -296,12 +296,12 @@ export default function CompressionChart(props: {
296296 <td>{r.ratio.toFixed(3)}</td>
297297 </tr>
298298 ))}
299- {refBits !== null && refBits > 0 && (
299+ {rateBits !== null && rateBits > 0 && (
300300 <tr>
301- <td>reference rate R (Monte-Carlo)</td>
301+ <td>entropy rate R (Monte-Carlo)</td>
302302 <td>—</td>
303- <td>{refBits.toFixed(3)}</td>
304- <td>{(16 / refBits).toFixed(3)}</td>
303+ <td>{rateBits.toFixed(3)}</td>
304+ <td>{(16 / rateBits).toFixed(3)}</td>
305305 </tr>
306306 )}
307307 </tbody>
src/components/MethodNote.tsxmodified+5−5View file
@@ -9,7 +9,7 @@ function Tex({ tex, display }: { tex: string; display?: boolean }) {
99 return <span dangerouslySetInnerHTML={{ __html: html }} />
1010 }
1111
12-/** Where the reference rate R comes from, in brief. */
12+/** Where the entropy rate R comes from, in brief. */
1313 export default function MethodNote() {
1414 return (
1515 <div className="math-section">
@@ -33,10 +33,10 @@ export default function MethodNote() {
3333 honest standard error.
3434 </p>
3535 <p className="card-note">
36- The estimate button under the compression chart runs exactly this method in a web
37- worker — a TypeScript port of the package (src/entropy, hand-synced), one independent
38- past at a time until stopped. The command line runs the Python original at the same
39- settings for an independent check.
36+ The estimate button beside the R readout runs exactly this method in a web worker — a
37+ TypeScript port of the package (src/entropy, hand-synced), one independent past at a
38+ time until stopped. The command line runs the Python original at the same settings for
39+ an independent check.
4040 </p>
4141 </div>
4242 )
src/components/useReferenceRate.ts →src/components/useEntropyRate.tsrenamed+5−5View file
@@ -6,13 +6,13 @@ import type { EntropyRequest, EntropyUpdate } from '../worker/entropyWorker'
66 * so a stop/start pair reproduces an uninterrupted run exactly. */
77 const BASE_SEED = 20260731
88
9-export interface ReferenceRate {
9+export interface EntropyRate {
1010 /** One unbiased estimate per independent past, in completion order. */
1111 perPast: number[]
1212 mean: number | null
1313 se: number | null
1414 running: boolean
15- /** "past 3 · rep 5/8" while computing. */
15+ /** "rep 5/8" within the current past while computing. */
1616 progress: string | null
1717 /** Conditioning window M. */
1818 past: number
@@ -21,11 +21,11 @@ export interface ReferenceRate {
2121 }
2222
2323 /**
24- * The in-browser reference-rate estimate: a worker refines it (one
24+ * The in-browser entropy-rate estimate: a worker refines it (one
2525 * independent past at a time) until stopped, and any change to the model
2626 * invalidates both the values and a run in flight.
2727 */
28-export function useReferenceRate(kernel: Float64Array, sigma: number): ReferenceRate {
28+export function useEntropyRate(kernel: Float64Array, sigma: number): EntropyRate {
2929 const [perPast, setPerPast] = useState<number[]>([])
3030 const [running, setRunning] = useState(false)
3131 const [progress, setProgress] = useState<string | null>(null)
@@ -55,7 +55,7 @@ export function useReferenceRate(kernel: Float64Array, sigma: number): Reference
5555 perPastRef.current = [...perPastRef.current, u.value]
5656 setPerPast(perPastRef.current)
5757 } else {
58- setProgress(`past ${u.pastIndex + 1} · rep ${u.repsDone}/${u.reps}`)
58+ setProgress(`rep ${u.repsDone}/${u.reps}`)
5959 }
6060 }
6161 workerRef.current = worker
src/entropy/index.tsmodified+1−1View file
@@ -1,5 +1,5 @@
11 /**
2- * In-browser unbiased estimation of H(z_{M+1} | z_1..z_M) — the reference
2+ * In-browser unbiased estimation of H(z_{M+1} | z_1..z_M) — the entropy
33 * rate R — for the app's model. Hand-synced TypeScript port of the sibling
44 * timeseries-entropy package (see the headers of estimator.ts / model.ts);
55 * defaults match its CLI so browser runs and `timeseries-entropy` runs
src/worker/entropyWorker.tsmodified+1−1View file
@@ -1,5 +1,5 @@
11 /**
2- * The reference-rate estimator off the main thread: one message starts an
2+ * The entropy-rate estimator off the main thread: one message starts an
33 * endless refinement loop that posts one unbiased value per completed past
44 * (with per-rep progress in between). Stopping is worker termination — the
55 * loop never yields, but outgoing messages still flow, and per-past values