ANS Visualizer#
Asymmetric Numeral Systems (ANS) is a family of entropy coding methods used in data compression. It was invented by Jarek Duda.
For arrays of independently sampled symbols, ANS can achieve compression ratios that approach the theoretical limit defined by Shannon entropy. ANS has been adopted in various compression algorithms and formats, including Facebook's Zstandard and Apple's LZFSE.
The simple_ans package is a simple and efficient Python implementation of ANS. The purpose of this app is to visualize the version of ANS encoding and decoding that is implemented in this library.
Explanation of 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 are the symbols we want to encode (in the interactive view, these are denoted as ), and assume that they occur with relative frequencies (positive integers). The goal is to encode a sequence of symbols into a single integer state .
We assume that the symbols are sampled randomly and independently of one another. Let
be the sum of the symbol frequencies.
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 that maps natural numbers to symbols:
We construct this table with a periodic pattern where:
- Each period of length consists of consecutive blocks:
- occurrences of symbol 0 (or A)
- occurrences of symbol 1 (or B)
- ...
- occurrences of symbol
- This pattern repeats every positions
- For any position ,
- Define as the cumulative frequency (start position of symbol in each period)
Encoding#
The encoding process works by maintaining a state value that evolves as we process each symbol. Starting from state :
- To encode symbol :
- Find the -th occurrence of symbol (counting from 0)
- This position index becomes our new state
- 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
where denotes integer division and is the modulo operation.
Notice that for large ,
where is the probability of symbol . This means that the expected value of grows by at each step, leading to:
Or in other words, if is the average number of bits required to encode a single symbol in the sequence, then
which matches the Shannon entropy of the symbol distribution. In other words, the integer encodes the state using the theoretically optimal number of bits.
Decoding#
Given a final state , we can recover the original sequence as follows:
- The last symbol is simply (the symbol at position )
- To get the previous state :
- Count how many times appeared before position
- This count is our previous state
- Continue this process to recover all symbols in reverse order
The formula for this decoding is
Interactive Visualization#
This app illustrates the encoding and decoding processes described above. You specify the symbol frequencies and the table of states is shown with each integer state represented as a box. As you hover over a box, you can see the path of black arrows representing the decoding process as well as blue arrows representing the encoding process for all possibilities of the next symbol to encode. For technical reasons, you also need to specify the number of leading A's for the decoded sequence, but this information cannot be captured by just the integer state alone.
Practical ANS Implementation#
The Need for Bounded State#
In practice, we can't work with arbitrarily large integers. Our state 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 be an array of 32-bit words, initialized to empty. We define:
STATE_BITS = 64(total bits for state)WORD_BITS = 32(bits per output word)THRESHOLD = 2^32(minimum state value after normalization)
For efficiency of calculations we assume that is a power of 2:
Encoding Process#
When encoding symbol with current state :
-
Check for normalization: If , then:
- Extract the lower 32 bits:
- Append to word array
- Update state:
-
Encode the symbol:
- Compute:
- Compute:
- Update state:
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 and word array :
-
Extract symbol:
- Find symbol such that
-
Compute previous state:
-
Check for denormalization: If and words remain:
- Pop word from end of
- Update:
-
Continue: Set 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:
- The assumption that the frequencies sum to a power of two
- The need to normalize the state
The first of these can be addressed by choosing large enough so that the proportions 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)