Skip to content

feat(entropy): entropy-coding research framework for RVQ/SPIHT priors#62

Merged
apage224 merged 3 commits into
mainfrom
feat/entropy-encoders
Jul 6, 2026
Merged

feat(entropy): entropy-coding research framework for RVQ/SPIHT priors#62
apage224 merged 3 commits into
mainfrom
feat/entropy-encoders

Conversation

@apage224

@apage224 apage224 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

In-flight research work exploring entropy coding as a swappable second stage on top of the existing frozen codecs (RVQ, SPIHT, Hybrid) — never touches reconstruction quality, only how compactly a codec's own output symbols are packed into bits.

This is not proposing a change to any golden/production configuration yet. It's the framework, tooling, and a set of research findings, in good shape for review before deciding what (if anything) gets promoted to a golden release.

What's included

  • compressionkit/runtime/entropy_algorithms.py — pluggable EntropyCoder-conforming wrappers: LearnedEntropyCoder (AI prior + arithmetic/rANS backend) and classical StaticHistogramEntropyCoder/MarkovEntropyCoder baselines, so "does the AI prior actually help" can be measured by swapping one object.
  • compressionkit/generative/causal_priors.py — causal token-prior architectures (WaveNet/GRU/CNN/DSCNN/hybrid variants) sharing a uniform (B, context_length) -> (B, context_length, vocab_size) interface.
  • compressionkit/generative/spiht_bit_prior.py — a pure-numpy inference path for a neural bit-level prior on SPIHT's bitstream, injected via a new generic sink/source dependency-injection seam added to compressionkit/dsp/spiht.py (SPIHT's tree-traversal algorithm stays entirely neural-agnostic).
  • compressionkit/runtime/prior.py, compressionkit/runtime/two_stage.py — real correctness fixes:
    • predict_log_probs/_get_probs now use the same windowed-context convention as real incremental/online inference (previously used a full unmasked vectorized shift that didn't match actual context-truncation behavior).
    • Fixed a zero-width-interval bug in the CDF quantization (_CDF_TOTAL vs _WHOLE) that could cause an infinite renormalization loop with confident, real (non-uniform) trained-prior probabilities.
    • Added a "rans" backend alongside the existing arithmetic coder.
  • docs/entropy-coding-research.md — the research framework doc: blocks, ready-made experiments, golden-promotion path, and "known lessons" (single-level token bug, LR-schedule/epoch-vs-steps pitfalls, augmentation reuse, validation-split leakage) so these aren't re-derived in a future session.
  • Research scripts (scripts/measure_rvq_entropy.py additions, scripts/measure_rvq_entropy_2level.py, scripts/benchmark_rans_vs_arithmetic.py, scripts/build_augmented_ecg_train_cache.py) and corresponding tests (47 new/modified across tests/test_causal_priors.py, tests/test_entropy_algorithms.py, tests/test_entropy_benchmark.py, tests/test_spiht_bit_prior.py, tests/test_two_stage.py).

Research findings (see docs/entropy-coding-research.md for detail)

  • RVQ entropy coding gives a real, substantial uplift (~1.6-1.9x on top of the codec's own compression) at a genuine ~60s streaming context; gains saturate around 60s (120s buys nothing further, 30s leaves some on the table).
  • WaveNet was the most consistent architecture across CR8/CR32 x ECG/PPG at a fixed 60s context — GRU is competitive at short contexts but degraded at the longest context tested (3840 tokens), including one run that came out worse than the free unigram baseline. That's flagged as needing further investigation (possibly BPTT vanishing-gradient related), not treated as a confirmed result.
  • SPIHT/Hybrid's neural bit-prior gives smaller but real gains (~3-17%, growing with CR) from a tiny (~2K parameter) model.
  • Classical arithmetic coding slightly outperforms rANS in the current implementation (~1% fewer bits) — no reason to prefer rANS yet.

Test/lint status

  • uv run pytest -q: 488 passed.
  • uv run ruff check . / uv run ruff format --check .: clean (fixed a real missing-import bug in spiht.py plus lint/format drift in 6 files as part of this PR).

Not in this PR

  • No change to any golden registry entry, deploy manifest, or production default — this is research infrastructure and findings only.
  • The GRU long-context anomaly (PPG CR8, 960-token context) needs a follow-up run (e.g. with gradient clipping) before drawing conclusions from it.

apage224 and others added 2 commits July 5, 2026 16:09
…ithmetic benchmarks)

Checkpoint of in-progress work handed off between agent sessions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes for PR readiness:
- compressionkit/dsp/spiht.py: add missing `Any` import (F821 — used in the
  new sink/source type hints, previously undefined at lint time).
- Apply ruff --fix for unused imports/noqa, RUF005/RUF046 style fixes.
- Apply ruff format to bring 6 files (written without the formatter) into
  compliance with the repo formatting convention.

No behavioral changes; full test suite (488 tests) still passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a research-oriented, pluggable entropy-coding layer that can sit losslessly on top of existing frozen codecs (RVQ/SPIHT/Hybrid), including learned priors, two backend bit-packers (arithmetic + rANS), SPIHT sink/source injection for neural bit priors, and a benchmark harness + documentation to support repeatable experiments without touching golden/production defaults.

Changes:

  • Introduces EntropyCoder-conforming implementations (learned + classical baselines) plus an evaluation harness for scorecard-style comparisons and noise sweeps.
  • Extends the two-stage runtime with correctness fixes (CDF quantization) and a new "rans" backend, and updates prior APIs to align with sliding-context incremental inference.
  • Adds SPIHT neural bit-prior numpy inference + injectable sink/source seam, along with extensive tests, research scripts, and documentation.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
zensical.toml Adds the entropy-coding research doc to the documentation nav.
docs/entropy-coding-research.md Documents the entropy-coding research “blocks → experiments → golden promotion” workflow and known pitfalls.
AGENTS.md Adds explicit guidance to reuse existing compressionkit/ blocks and avoid duplicated script logic.
compressionkit/runtime/entropy_algorithms.py Adds pluggable entropy coders (learned prior wrapper + static/Markov baselines) using arithmetic/rANS backends.
compressionkit/evaluation/entropy_benchmark.py Adds a benchmark harness to evaluate entropy coders on token streams and across SNR/noise conditions.
compressionkit/runtime/prior.py Adds predict_next_probs() and updates predict_log_probs() to use sliding context/incremental convention.
compressionkit/runtime/two_stage.py Adds "rans" backend, fixes CDF quantization total to avoid zero-width intervals, and aligns probability extraction with incremental context.
compressionkit/dsp/spiht.py Adds generic sink=/source= dependency injection seam and bitplane hook support for neural entropy coding.
compressionkit/generative/causal_priors.py Adds causal prior architectures and SplitHalf layer to avoid Lambda-serialization fragility; includes SPIHT bit-prior builder.
compressionkit/generative/spiht_bit_prior.py Adds pure-numpy WaveNet bit-prior inference plus NeuralAcSink/NeuralAcSource and incremental predictor.
compressionkit/generative/init.py Re-exports causal-prior builders and SplitHalf from the package namespace.
scripts/measure_rvq_entropy.py Adds LR schedule + steps_per_epoch support and dataset-repeat training flow for prior training experiments.
scripts/measure_rvq_entropy_2level.py Adds a 2-level (time × level) RVQ prior experiment that avoids token interleaving.
scripts/benchmark_rans_vs_arithmetic.py Adds an experiment to compare actual rANS vs arithmetic bit usage using trained priors and cached token streams.
scripts/build_augmented_ecg_train_cache.py Adds a script to build an augmented ECG token cache by reusing the codec’s own training pipeline/augmentation.
tests/test_two_stage.py Adds regression coverage for the CDF zero-width bug and new rANS backend roundtrips + backend selection errors.
tests/test_entropy_algorithms.py Adds protocol/roundtrip tests for learned/static/Markov entropy coders across arithmetic/rANS.
tests/test_entropy_benchmark.py Adds tests for the benchmark harness and noise sweep behavior.
tests/test_causal_priors.py Adds save/load and trainability regression tests for WaveNet priors (avoids Lambda deserialization breakage).
tests/test_spiht_bit_prior.py Adds correctness + integration tests for SPIHT neural bit prior (numpy forward pass, incremental predictor, sink/source roundtrip).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread compressionkit/runtime/two_stage.py Outdated
Comment on lines 175 to 179
@@ -149,11 +178,15 @@
indices_4d = indices[np.newaxis]
bpt_prior = self._prior.bits_per_token(indices_4d)
Comment on lines +636 to +642
slot = x & (_RANS_TOTAL - 1)
sym = 0
for s in range(vocab_size):
if starts[s] <= slot < starts[s] + freqs[s]:
sym = s
break
decoded_tokens[t] = sym
Comment on lines +528 to 533
if sink is not None:
if log_emissions:
raise ValueError("sink and log_emissions are mutually exclusive.")
elif log_emissions:
if not use_ac:
raise ValueError("log_emissions=True requires use_ac=True")
Comment on lines +145 to +147
def predict_next_probs(self, context_tokens: np.ndarray) -> np.ndarray:
batch = context_tokens.shape[0]
return np.tile(self._probs, (batch, 1))
Comment thread scripts/measure_rvq_entropy_2level.py Outdated
Comment on lines +187 to +195
# Level-1 head sees h_t (causal state) AND the *next* step's true level0
# embedding (teacher forcing) — same trick a coarse-to-fine / residual
# token model uses. Pad the shift with a learned "unknown" embedding so
# shapes stay static (last position has no "next" ground truth).
next_level0_embed = embed0(level0_in) # reuse table; shift below
shifted = keras.layers.Lambda(
lambda t: keras.ops.pad(t[:, 1:, :], [[0, 0], [0, 1], [0, 0]]),
name="shift_level0_embed",
)(next_level0_embed)
@apage224
apage224 marked this pull request as ready for review July 5, 2026 20:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0c0287565

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/measure_rvq_entropy_2level.py Outdated
x1 = np.stack([level1[s : s + context_length] for s in bs]).astype(np.int32)
logits0, logits1 = model([x0, x1], training=False)
last0 = keras.ops.convert_to_numpy(logits0[:, -1, :]).astype(np.float64)
last1 = keras.ops.convert_to_numpy(logits1[:, -1, :]).astype(np.float64)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Evaluate level-1 NLL with the true level-0 code

When running the 2-level entropy script, level1_logits[:, -1, :] scores the only position where shift_level0_embed has padded away the “next” level-0 embedding, so every validation level-1 token is measured without the same-step level-0 code that the model is trained and documented to use. This makes entropy_report.json report a different, weaker distribution for level-1 and can invalidate comparisons against the 1-D prior; score a position whose shifted level-0 token is present, or append/provide the target level-0 before evaluating level-1.

Useful? React with 👍 / 👎.

Addresses Copilot/Codex review findings on PR #62:

- two_stage.py: compress_indices() no longer does a second full prior pass
  to compute bits_per_token — derives it directly from the already-computed
  probs_per_position (same windowed-context values), halving TFLite
  interpreter calls per encode.
- two_stage.py: rANS decode now finds each token via binary search
  (bisect) over the strictly-increasing cumulative starts array instead of
  an O(vocab_size) linear scan per token.
- spiht.py: injecting a custom sink now requires use_ac=True (previously
  only documented, not enforced) — prevents silently producing an
  undecodable bitstream whose metadata says use_ac=False.
- entropy_algorithms.py: fixed _StaticPriorAdapter docstring to match
  actual behavior (it always returns the fitted histogram, including at
  position 0 — the callers, not this adapter, are what special-case
  position 0 elsewhere).
- measure_rvq_entropy_2level.py: replaced a closure-based keras.layers.Lambda
  with Cropping1D + ZeroPadding1D (the same fragile-serialization pattern
  this PR introduced SplitHalf to avoid elsewhere).
- measure_rvq_entropy_2level.py: fixed a real evaluation bug — the entropy
  eval scored level-1 at the last window position, which is exactly the one
  position per window where the teacher-forced level-0 side info is
  necessarily the zero/unknown padding (the window never contains the
  token one-beyond-itself). This systematically understated the 2-level
  model, invalidating comparisons against the 1-D prior. Now scores level-1
  at the second-to-last position, where the teacher-forced level-0 code is
  real.

All 488 tests still pass; ruff check/format clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@apage224

apage224 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all 6 findings from the Copilot review (commit f9f496d):

  • Removed a double prior-pass in compress_indices() (perf)
  • rANS decode now uses binary search instead of an O(vocab_size) linear scan (perf)
  • Custom sink injection in spiht.py now requires use_ac=True, enforced rather than just documented (safety)
  • Fixed _StaticPriorAdapter docstring/behavior mismatch
  • Replaced a closure-based Lambda in measure_rvq_entropy_2level.py with Cropping1D+ZeroPadding1D (serialization robustness)
  • Fixed a real evaluation bug in the 2-level entropy script: level-1 was being scored at exactly the one window position where its teacher-forced side info is structurally unavailable, understating the model. Now scored at the correct position.

All 488 tests pass, ruff check/format clean, CI green. Merging.

@apage224
apage224 merged commit cb7bdfd into main Jul 6, 2026
1 check passed
@apage224
apage224 deleted the feat/entropy-encoders branch July 6, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants