/ concept-collection / ans-visualizer
Sign in
concept-collection / ans-visualizer
ans-visualizer / src / explanation.md
5.9 KBPreviewCodeBlameHistoryRaw

Explanation of Asymmetric Numeral Systems (ANS)#

The core idea of ANS is to encode a sequence of symbols into a single integer state, with each symbol affecting the state in a way that reflects its probability of occurrence. This encoding is reversible, allowing us to recover the original sequence from the coded integer.

Suppose that {0,1,...,S1}\{0, 1, ..., S-1\} are the symbols we want to encode (in this UI these are denoted as A,B,C,...A, B, C, ...), and assume that they occur with relative frequencies f0,...,fS1f_0, ..., f_{S-1} (positive integers). The goal is to encode a sequence of symbols (s1,s2,...,sk)(s_1, s_2, ..., s_k) into a single integer state xx.

We assume that the symbols are sampled randomly and independently of one another. Let

L=i=0S1fiL = \sum_{i=0}^{S-1} f_i

be the sum of the symbol frequencies.

ANS Encoding/Decoding#

The Symbol Table#

To encode our symbols efficiently, we need a way to map between states and symbols that reflects their frequencies. We accomplish this using an infinite table TT that maps natural numbers to symbols: T:N{0,...,S1}T: \mathbb{N} \to \{0,...,S-1\}

We construct this table with a periodic pattern where:

The Encoding/Decoding Process#

The encoding process works by maintaining a state value xx that evolves as we process each symbol. Starting from state x0x_0:

  1. To encode symbol s1s_1:
    • Find the x0x_0-th occurrence of symbol s1s_1 (counting from 0)
    • This position index becomes our new state x1x_1
  2. Repeat this process for each new symbol, using the previous state to find the next one

This process effectively "pushes" each symbol onto our state value in a way that can be reversed.

With the periodic structured table, the formula for this is

xi+1=(xi//fsi)L+C(si)+(xi mod fsi)x_{i+1} = (x_i // f_{s_i}) \cdot L + C(s_i) + (x_i \text{ mod } f_{s_i})

where //// denotes integer division and  mod \text{ mod } is the modulo operation.

Notice that for large xix_i,

xi+1xips1x_{i+1} \approx x_i \cdot p_s^{-1}

where ps=fs/Lp_s = f_s / L is the probability of symbol ss. This means that the expected value of log(xi)\log(x_i) grows by log(ps)-\log(p_s) at each step, leading to:

limkE(1klog(xk))=s=0S1pslog(ps).\lim_{k \to \infty} E(\frac{1}{k}\log(x_k)) = -\sum_{s=0}^{S-1} p_s \log(p_s).

Or in other words, if HH is the average number of bits required to encode a single symbol in the sequence, then

H=s=0S1pslog(ps)H = -\sum_{s=0}^{S-1} p_s \log(p_s)

which matches the Shannon entropy of the symbol distribution. In other words, the integer xkx_k encodes the state using the theoretically optimal number of bits.

Now for decoding. Given a final state xkx_k, we can recover the original sequence as follows:

  1. The last symbol sks_k is simply T[xk]T[x_k] (the symbol at position xkx_k)
  2. To get the previous state xk1x_{k-1}:
    • Count how many times sks_k appeared before position xkx_k
    • This count is our previous state xk1x_{k-1}
  3. Continue this process to recover all symbols in reverse order

The formula for this decoding is

xi=(xi+1//L)fsi+(xi+1 mod L)C(si)x_i = (x_{i+1} // L) \cdot f_{s_i} + (x_{i+1} \text{ mod } L) - C(s_i)

Practical ANS Implementation#

The Need for Bounded State#

In practice, we can't work with arbitrarily large integers. Our state xx would grow indefinitely as we encode more symbols, and operating on integers with arbitrarily large precision is very inefficient. We therefore need a way to keep the state within a manageable range while preserving the reversibility of the encoding.

Word-Based Streaming with Conditional Normalization#

Instead of normalizing at every step, we use a more efficient approach that only normalizes when necessary. We maintain our state as a 64-bit integer and stream out 32-bit words when the state would overflow.

Let WW be an array of 32-bit words, initialized to empty. We define:

For efficiency of calculations we assume that LL is a power of 2:

L=2lL = 2^{l}

Encoding Process#

When encoding symbol ss with current state xx:

  1. Check for normalization: If (x>>(64l))fs(x >> (64 - l)) \geq f_s, then:

    • Extract the lower 32 bits: w=x mod 232w = x \text{ mod } 2^{32}
    • Append ww to word array WW
    • Update state: x=x>>32x = x >> 32
  2. Encode the symbol:

    • Compute: remainder=x mod fsremainder = x \text{ mod } f_s
    • Compute: prefix=x//fsprefix = x // f_s
    • Update state: x=(prefix<<l)(Cs+remainder)x = (prefix << l) | (C_s + remainder)

The key insight is that normalization only occurs when the next encoding step would cause overflow, making the algorithm more efficient.

Decoding Process#

To decode from final state xx and word array WW:

  1. Extract symbol:

    • quantile=x mod Lquantile = x \text{ mod } L
    • Find symbol ss such that Csquantile<Cs+fsC_s \leq quantile < C_s + f_s
  2. Compute previous state:

    • prefix=x>>lprefix = x >> l
    • previous_state=prefixfs+(quantileCs)previous\_state = prefix \cdot f_s + (quantile - C_s)
  3. Check for denormalization: If previous_state<THRESHOLDprevious\_state < THRESHOLD and words remain:

    • Pop word ww from end of WW
    • Update: previous_state=(previous_state<<32)wprevious\_state = (previous\_state << 32) | w
  4. Continue: Set x=previous_statex = previous\_state and repeat

This approach maintains the state above the threshold while efficiently managing memory by streaming out words only when necessary.

Optimality of the Compression#

In the above, there are two sources of loss in the compression efficiency:

  1. The assumption that the frequencies sum to a power of two
  2. The need to normalize the state

The first of these can be addressed by choosing L=2lL=2^l large enough so that the proportions pi=fi/Lp_i = f_i / L are accurate enough so that the loss is negligible.

The second source of loss is more difficult to predict but can be examined empirically. (not yet explored)

moveopenescclose