GGDLPC 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ggdlpc-0.0.1/PKG-INFO +64 -0
- ggdlpc-0.0.1/README.md +49 -0
- ggdlpc-0.0.1/pyproject.toml +29 -0
- ggdlpc-0.0.1/setup.cfg +4 -0
- ggdlpc-0.0.1/src/GGDLPC/__init__.py +28 -0
- ggdlpc-0.0.1/src/GGDLPC/_core.py +159 -0
- ggdlpc-0.0.1/src/GGDLPC/cbridge.py +227 -0
- ggdlpc-0.0.1/src/GGDLPC/codec.py +124 -0
- ggdlpc-0.0.1/src/GGDLPC/fit.py +334 -0
- ggdlpc-0.0.1/src/GGDLPC/ggdlpc.c +644 -0
- ggdlpc-0.0.1/src/GGDLPC/ggdlpc.h +136 -0
- ggdlpc-0.0.1/src/GGDLPC/maps.py +134 -0
- ggdlpc-0.0.1/src/GGDLPC/proxy.py +161 -0
- ggdlpc-0.0.1/src/GGDLPC/reference.py +180 -0
- ggdlpc-0.0.1/src/GGDLPC/selftest.py +141 -0
- ggdlpc-0.0.1/src/GGDLPC/tables.py +168 -0
- ggdlpc-0.0.1/src/GGDLPC.egg-info/PKG-INFO +64 -0
- ggdlpc-0.0.1/src/GGDLPC.egg-info/SOURCES.txt +19 -0
- ggdlpc-0.0.1/src/GGDLPC.egg-info/dependency_links.txt +1 -0
- ggdlpc-0.0.1/src/GGDLPC.egg-info/requires.txt +3 -0
- ggdlpc-0.0.1/src/GGDLPC.egg-info/top_level.txt +1 -0
ggdlpc-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: GGDLPC
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Lossless compression via generalized Gaussian modeling of linear predictive coding residuals
|
|
5
|
+
Author-email: Dan Jacobellis <danjacobellis@utexas.edu>
|
|
6
|
+
Project-URL: Homepage, https://danjacobellis.net
|
|
7
|
+
Project-URL: Repository, https://github.com/danjacobellis/GGDLPC
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: scipy
|
|
14
|
+
Requires-Dist: torch
|
|
15
|
+
|
|
16
|
+
# GGDLPC
|
|
17
|
+
|
|
18
|
+
**Generalized Gaussian Distribution Linear Predictive Coding** — a lossless compression method for scalar-quantized signals, autoencoder latents, and neural network parameter values.
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
The core observation is that prediction residuals of many integer-valued signals — particularly quantized autoencoder latents — are well-approximated by a Generalized Gaussian Distribution (GGD). Since the KL divergence between the actual distribution and a fitted GGD is small (~0.75% above the conditional entropy bound), Huffman codes designed for the GGD are near-optimal for the actual data.
|
|
23
|
+
|
|
24
|
+
**Linear prediction** removes spatial/temporal correlation: each sample is predicted as a linear combination of causal neighbors, and the prediction residual is entropy coded.
|
|
25
|
+
|
|
26
|
+
**Per-channel parametric model**: each channel is described by a small number of scalars:
|
|
27
|
+
- A **scale relationship**: the GGD scale parameter varies as an affine function of a local activity statistic
|
|
28
|
+
- A **shape relationship**: the GGD shape parameter β varies as an affine function of log-scale
|
|
29
|
+
- Linear predictor weights and bias
|
|
30
|
+
|
|
31
|
+
From these scalars, all Huffman tables across ~41 log-spaced context bins are **generated analytically** at load time — no fitted tables are stored or transmitted.
|
|
32
|
+
|
|
33
|
+
**Embedded run mode**: for contexts where the conditional entropy falls below 1 bit (where symbol codes are inherently wasteful), a zero-run mode with elementary Golomb codes recovers the sub-bit rate, with the trigger and order derived from the same parametric model.
|
|
34
|
+
|
|
35
|
+
## Design principles (from LOCO-I/JPEG-LS)
|
|
36
|
+
|
|
37
|
+
1. **Structure**: prediction → context statistic → parametric conditional model
|
|
38
|
+
2. **Model cost**: few parameters per channel, with the model generating every context's distribution
|
|
39
|
+
3. **Sufficient statistics**: code selection from a decoder-computable causal statistic — no signaling
|
|
40
|
+
4. **Symbol codes only**: canonical Huffman + Golomb family — table lookups and shifts, no multiplications, suitable for FPGA and microcontroller targets
|
|
41
|
+
|
|
42
|
+
## Origin
|
|
43
|
+
|
|
44
|
+
GGDLPC was developed as the entropy coding stage for the [FRAPPE](https://ut-sysml.github.io/FRAPPE/) family of asymmetric neural codecs (v3+), replacing the off-the-shelf JPEG-LS codec used in earlier versions. It applies to any integer-valued signal with GGD-distributed prediction residuals.
|
|
45
|
+
|
|
46
|
+
## Package
|
|
47
|
+
|
|
48
|
+
`src/GGDLPC/` is the Python package (C coding engine JIT-built on first import; requires a C compiler). One call codes one integer tensor (values in [-N, N], N ≤ 65536 chosen at fit time; rank 1, 2, or 3) as one self-contained byte-padded bitstream, truncatable at any channel boundary. Macroregions, pre-quantization, multi-call file layouts, and metadata are caller compositions.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import GGDLPC
|
|
52
|
+
channels, prov = GGDLPC.fit(loader_factory, N=31, rank=2)
|
|
53
|
+
codec = GGDLPC.Codec(GGDLPC.new_blob(31, 2, channels, prov))
|
|
54
|
+
data, ch_bits = codec.encode(z) # (C, *S) integers -> bytes
|
|
55
|
+
z2 = codec.decode(data, z.shape) # bit-exact
|
|
56
|
+
bits = GGDLPC.proxy_call_bits(z_noisy, codec.blob) # differentiable rate
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Local development install: `./install_for_debugging.sh` (builds a wheel, installs into the `~/g` venv, runs `GGDLPC.selftest()`).
|
|
60
|
+
|
|
61
|
+
## Related
|
|
62
|
+
|
|
63
|
+
- [FRAPPEv5](https://github.com/danjacobellis/FRAPPEv5) — autoencoder training code that uses GGDLPC
|
|
64
|
+
- [compressors](https://github.com/danjacobellis/compressors) — codec library with FRAPPE v1–v3 inference implementations
|
ggdlpc-0.0.1/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# GGDLPC
|
|
2
|
+
|
|
3
|
+
**Generalized Gaussian Distribution Linear Predictive Coding** — a lossless compression method for scalar-quantized signals, autoencoder latents, and neural network parameter values.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
The core observation is that prediction residuals of many integer-valued signals — particularly quantized autoencoder latents — are well-approximated by a Generalized Gaussian Distribution (GGD). Since the KL divergence between the actual distribution and a fitted GGD is small (~0.75% above the conditional entropy bound), Huffman codes designed for the GGD are near-optimal for the actual data.
|
|
8
|
+
|
|
9
|
+
**Linear prediction** removes spatial/temporal correlation: each sample is predicted as a linear combination of causal neighbors, and the prediction residual is entropy coded.
|
|
10
|
+
|
|
11
|
+
**Per-channel parametric model**: each channel is described by a small number of scalars:
|
|
12
|
+
- A **scale relationship**: the GGD scale parameter varies as an affine function of a local activity statistic
|
|
13
|
+
- A **shape relationship**: the GGD shape parameter β varies as an affine function of log-scale
|
|
14
|
+
- Linear predictor weights and bias
|
|
15
|
+
|
|
16
|
+
From these scalars, all Huffman tables across ~41 log-spaced context bins are **generated analytically** at load time — no fitted tables are stored or transmitted.
|
|
17
|
+
|
|
18
|
+
**Embedded run mode**: for contexts where the conditional entropy falls below 1 bit (where symbol codes are inherently wasteful), a zero-run mode with elementary Golomb codes recovers the sub-bit rate, with the trigger and order derived from the same parametric model.
|
|
19
|
+
|
|
20
|
+
## Design principles (from LOCO-I/JPEG-LS)
|
|
21
|
+
|
|
22
|
+
1. **Structure**: prediction → context statistic → parametric conditional model
|
|
23
|
+
2. **Model cost**: few parameters per channel, with the model generating every context's distribution
|
|
24
|
+
3. **Sufficient statistics**: code selection from a decoder-computable causal statistic — no signaling
|
|
25
|
+
4. **Symbol codes only**: canonical Huffman + Golomb family — table lookups and shifts, no multiplications, suitable for FPGA and microcontroller targets
|
|
26
|
+
|
|
27
|
+
## Origin
|
|
28
|
+
|
|
29
|
+
GGDLPC was developed as the entropy coding stage for the [FRAPPE](https://ut-sysml.github.io/FRAPPE/) family of asymmetric neural codecs (v3+), replacing the off-the-shelf JPEG-LS codec used in earlier versions. It applies to any integer-valued signal with GGD-distributed prediction residuals.
|
|
30
|
+
|
|
31
|
+
## Package
|
|
32
|
+
|
|
33
|
+
`src/GGDLPC/` is the Python package (C coding engine JIT-built on first import; requires a C compiler). One call codes one integer tensor (values in [-N, N], N ≤ 65536 chosen at fit time; rank 1, 2, or 3) as one self-contained byte-padded bitstream, truncatable at any channel boundary. Macroregions, pre-quantization, multi-call file layouts, and metadata are caller compositions.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import GGDLPC
|
|
37
|
+
channels, prov = GGDLPC.fit(loader_factory, N=31, rank=2)
|
|
38
|
+
codec = GGDLPC.Codec(GGDLPC.new_blob(31, 2, channels, prov))
|
|
39
|
+
data, ch_bits = codec.encode(z) # (C, *S) integers -> bytes
|
|
40
|
+
z2 = codec.decode(data, z.shape) # bit-exact
|
|
41
|
+
bits = GGDLPC.proxy_call_bits(z_noisy, codec.blob) # differentiable rate
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Local development install: `./install_for_debugging.sh` (builds a wheel, installs into the `~/g` venv, runs `GGDLPC.selftest()`).
|
|
45
|
+
|
|
46
|
+
## Related
|
|
47
|
+
|
|
48
|
+
- [FRAPPEv5](https://github.com/danjacobellis/FRAPPEv5) — autoencoder training code that uses GGDLPC
|
|
49
|
+
- [compressors](https://github.com/danjacobellis/compressors) — codec library with FRAPPE v1–v3 inference implementations
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "GGDLPC"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Lossless compression via generalized Gaussian modeling of linear predictive coding residuals"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Dan Jacobellis", email = "danjacobellis@utexas.edu"}
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Operating System :: OS Independent"
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"numpy",
|
|
19
|
+
"scipy",
|
|
20
|
+
"torch",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.10"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.package-data]
|
|
25
|
+
"GGDLPC" = ["*.c", "*.h"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://danjacobellis.net"
|
|
29
|
+
Repository = "https://github.com/danjacobellis/GGDLPC"
|
ggdlpc-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""GGDLPC -- lossless compression via generalized Gaussian modeling of
|
|
2
|
+
linear predictive coding residuals.
|
|
3
|
+
|
|
4
|
+
The primitive: one call encodes one integer tensor (values in [-N, N],
|
|
5
|
+
N <= 65536 chosen at fit time; rank 1/2/3) as one self-contained
|
|
6
|
+
byte-padded bitstream, truncatable at any channel boundary. Macroregions,
|
|
7
|
+
pre-quantization, multi-call file layouts, and metadata are caller
|
|
8
|
+
compositions.
|
|
9
|
+
|
|
10
|
+
import GGDLPC
|
|
11
|
+
channels, prov = GGDLPC.fit(loader_factory, N=31, rank=2)
|
|
12
|
+
codec = GGDLPC.Codec(GGDLPC.new_blob(31, 2, channels, prov))
|
|
13
|
+
data, ch_bits = codec.encode(z) # (C, *S) ints -> bytes
|
|
14
|
+
z2 = codec.decode(data, z.shape) # bit-exact
|
|
15
|
+
bits = GGDLPC.proxy_call_bits(z_noisy, codec.blob) # differentiable
|
|
16
|
+
"""
|
|
17
|
+
from ._core import (A_MAX, M_MIN, PAD_BITS, TAU, n_grid, stat_cap)
|
|
18
|
+
from .codec import Codec, SPEC, VERSION, new_blob
|
|
19
|
+
from .fit import fit
|
|
20
|
+
from .proxy import proxy_call_bits, cont_maps, ideal_bits
|
|
21
|
+
from .selftest import selftest
|
|
22
|
+
from .tables import build_channel, derive_split
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
'Codec', 'SPEC', 'VERSION', 'new_blob', 'fit', 'proxy_call_bits',
|
|
26
|
+
'cont_maps', 'ideal_bits', 'selftest', 'build_channel', 'derive_split',
|
|
27
|
+
'A_MAX', 'M_MIN', 'PAD_BITS', 'TAU', 'n_grid', 'stat_cap',
|
|
28
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""GGDLPC core math: derived constants, GGD machinery, Huffman lengths.
|
|
2
|
+
|
|
3
|
+
Everything is parameterized by the alphabet bound N (inputs are integers in
|
|
4
|
+
[-N, N], N <= 65536, chosen at fit time). Derived quantities:
|
|
5
|
+
|
|
6
|
+
- NSYM = 2N + 1: the folded residual alphabet (residuals in [-2N, 2N] are
|
|
7
|
+
reduced mod NSYM by the JPEG-LS fold).
|
|
8
|
+
- G = ceil(log_tau(2N / m_min)) + 1: scale-grid cells (tau = 2^{1/4},
|
|
9
|
+
m_min = 1/16). G = 41 at N = 31, 49 at N = 128, 81 at N = 32768,
|
|
10
|
+
85 at N = 65536.
|
|
11
|
+
- STAT_CAP = ceil(tau^{G-1}) - 1: cap on the integer statistic S = floor(16 s)
|
|
12
|
+
at the top of the scale grid (1023 at N = 31, 4095 at N = 128,
|
|
13
|
+
2^20 - 1 at N = 32768, 2^21 - 1 at N = 65536).
|
|
14
|
+
|
|
15
|
+
N = 65536 exists so that bit-depth-expanding lossless transforms of 16-bit
|
|
16
|
+
sources (mid-side stereo: the side channel L - R spans [-65535, 65535]) fit
|
|
17
|
+
the contract; sources above 16 bits are still pre-quantized by the caller.
|
|
18
|
+
|
|
19
|
+
The Huffman-length and GGD-integration numerics are copied from the FRAPPEv3
|
|
20
|
+
coder (redesign_rounds/sweep.py) so that at N = 31 the generated tables are
|
|
21
|
+
bit-identical to the shipped lawcoder's.
|
|
22
|
+
"""
|
|
23
|
+
import heapq
|
|
24
|
+
import math
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
from scipy.optimize import brentq
|
|
28
|
+
from scipy.special import gammainc, gammaln
|
|
29
|
+
|
|
30
|
+
TAU = 2 ** 0.25
|
|
31
|
+
M_MIN = 1.0 / 16.0
|
|
32
|
+
A_MAX = 63 # windowed hi alphabet (table size <= A_MAX + 1 escape)
|
|
33
|
+
N_MAX = 65536
|
|
34
|
+
MIN_OCC = 10_000 # minimum bucket occupancy for a Sharifi shape estimate
|
|
35
|
+
PAD_BITS = 3.5 # expected bits lost to one byte pad per call
|
|
36
|
+
Q_FLOOR = 1e-12
|
|
37
|
+
|
|
38
|
+
RANK_NF = {1: 2, 2: 5, 3: 9} # FIR feature count (template M) by rank
|
|
39
|
+
STAT_W = 16 # residual-window length of the pooled statistic
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def n_grid(N):
|
|
43
|
+
"""Number of scale-grid cells G for alphabet bound N."""
|
|
44
|
+
return int(math.ceil(4.0 * math.log2(2 * N / M_MIN) - 1e-9)) + 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def stat_cap(N):
|
|
48
|
+
"""Integer statistic cap: top of the scale grid, minus one LSB."""
|
|
49
|
+
G = n_grid(N)
|
|
50
|
+
return int(math.ceil(TAU ** (G - 1) - 1e-9)) - 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def ggd_cdf(x, beta):
|
|
54
|
+
return 0.5 + 0.5 * np.sign(x) * gammainc(1.0 / beta,
|
|
55
|
+
np.abs(x) ** beta + 1e-300)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def c_of_beta(beta):
|
|
59
|
+
"""Mean-absolute-deviation to GGD-scale conversion factor."""
|
|
60
|
+
return math.exp(gammaln(1.0 / beta) - gammaln(2.0 / beta))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sharifi_beta(rho):
|
|
64
|
+
"""Sharifi--Leon-Garcia shape estimate from rho = E(x-mu)^2 / (E|x-mu|)^2."""
|
|
65
|
+
def f(b):
|
|
66
|
+
return (gammaln(1.0 / b) + gammaln(3.0 / b)
|
|
67
|
+
- 2.0 * gammaln(2.0 / b)) - math.log(rho)
|
|
68
|
+
lo, hi = 0.05, 10.0
|
|
69
|
+
if f(lo) < 0:
|
|
70
|
+
return lo
|
|
71
|
+
if f(hi) > 0:
|
|
72
|
+
return hi
|
|
73
|
+
return brentq(f, lo, hi, xtol=1e-6)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def ggd_folded_probs(mu, beta, alpha, N):
|
|
77
|
+
"""Integer-bin GGD mass over three fold periods, folded mod (2N+1).
|
|
78
|
+
|
|
79
|
+
Returns the folded pmf over w in [0, 2N] (w = (r + N) mod (2N+1)),
|
|
80
|
+
floored at Q_FLOOR and normalized. At N = 31 this reproduces the
|
|
81
|
+
FRAPPEv3 sweep.ggd_folded_probs values exactly (the v3 index
|
|
82
|
+
(r + 31) % 63 equals this w). For large N the integration is
|
|
83
|
+
restricted to the density's numerical support (|x|^beta <= 45,
|
|
84
|
+
tail mass < e^-45 ~ 3e-20, far below Q_FLOOR); outside it the bins
|
|
85
|
+
take the floor, exactly as they would from a full evaluation."""
|
|
86
|
+
nsym = 2 * N + 1
|
|
87
|
+
half = (3 * nsym - 1) // 2
|
|
88
|
+
R = int(math.ceil(alpha * 45.0 ** (1.0 / beta) + abs(mu))) + 2
|
|
89
|
+
lo_r, hi_r = max(-half, -R), min(half, R)
|
|
90
|
+
r = np.arange(lo_r, hi_r + 1)
|
|
91
|
+
hi = ggd_cdf((r + 0.5 - mu) / alpha, beta)
|
|
92
|
+
lo = ggd_cdf((r - 0.5 - mu) / alpha, beta)
|
|
93
|
+
p = np.maximum(hi - lo, 0.0)
|
|
94
|
+
w = (r + N) % nsym
|
|
95
|
+
q = np.zeros(nsym)
|
|
96
|
+
np.add.at(q, w, p)
|
|
97
|
+
q = np.maximum(q, Q_FLOOR)
|
|
98
|
+
return q / q.sum()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def huffman_lengths(weights):
|
|
102
|
+
"""Exact Huffman code lengths (float array). Copied verbatim from the
|
|
103
|
+
FRAPPEv3 sweep.huffman_lengths (same heap tie-breaking) so generated
|
|
104
|
+
tables match the shipped coder bit for bit at N = 31."""
|
|
105
|
+
w = np.asarray(weights, np.float64)
|
|
106
|
+
idx = np.nonzero(w > 0)[0]
|
|
107
|
+
if len(idx) == 0:
|
|
108
|
+
return np.zeros_like(w)
|
|
109
|
+
if len(idx) == 1:
|
|
110
|
+
L = np.zeros_like(w)
|
|
111
|
+
L[idx[0]] = 1.0
|
|
112
|
+
return L
|
|
113
|
+
heap = [(w[i], n) for n, i in enumerate(idx)]
|
|
114
|
+
heapq.heapify(heap)
|
|
115
|
+
parent = {}
|
|
116
|
+
nxt = len(idx)
|
|
117
|
+
while len(heap) > 1:
|
|
118
|
+
w1, n1 = heapq.heappop(heap)
|
|
119
|
+
w2, n2 = heapq.heappop(heap)
|
|
120
|
+
parent[n1] = parent[n2] = nxt
|
|
121
|
+
heapq.heappush(heap, (w1 + w2, nxt))
|
|
122
|
+
nxt += 1
|
|
123
|
+
L = np.zeros_like(w)
|
|
124
|
+
depths = {heap[0][1]: 0}
|
|
125
|
+
for n in range(nxt - 1, -1, -1):
|
|
126
|
+
if n in parent:
|
|
127
|
+
depths[n] = depths[parent[n]] + 1
|
|
128
|
+
for n, i in enumerate(idx):
|
|
129
|
+
L[i] = depths[n]
|
|
130
|
+
L[w <= 0] = L.max()
|
|
131
|
+
return L
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def run_rate_fixed_g(p0, L_excl, g):
|
|
135
|
+
"""Model-expected bits/sample of EG_{2^g} zero-run coding (v3 formula)."""
|
|
136
|
+
if p0 >= 1.0:
|
|
137
|
+
return 1.0 / (1 << g)
|
|
138
|
+
m = 1 << g
|
|
139
|
+
pm = p0 ** m
|
|
140
|
+
cost = pm * 1.0 + (1.0 - pm) * (1.0 + g + L_excl)
|
|
141
|
+
x = p0
|
|
142
|
+
s_partial = (1 - (m + 1) * x ** m + m * x ** (m + 1)) / (1 - x) ** 2
|
|
143
|
+
samples = m * pm + (1.0 - x) * s_partial
|
|
144
|
+
return cost / samples
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def canonical_codes(lengths):
|
|
148
|
+
"""Canonical codewords for integer code lengths: increasing length,
|
|
149
|
+
then increasing symbol index (matches the C table construction)."""
|
|
150
|
+
lengths = np.asarray(lengths, np.int64)
|
|
151
|
+
order = sorted(range(len(lengths)), key=lambda s: (lengths[s], s))
|
|
152
|
+
codes = np.zeros(len(lengths), np.int64)
|
|
153
|
+
code, prev = 0, int(lengths[order[0]])
|
|
154
|
+
for sym in order:
|
|
155
|
+
code <<= int(lengths[sym]) - prev
|
|
156
|
+
prev = int(lengths[sym])
|
|
157
|
+
codes[sym] = code
|
|
158
|
+
code += 1
|
|
159
|
+
return codes
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""ctypes bridge to the GGDLPC C engine (lazy JIT build of libggdlpc).
|
|
2
|
+
|
|
3
|
+
The C library executes the full coding loops (prediction, statistic, cell
|
|
4
|
+
selection, quotient-split symbol coding, run mode); Python generates the
|
|
5
|
+
tables (tables.py) and passes them in. The library is compiled once on
|
|
6
|
+
first import from the vendored C sources (same pattern as the FRAPPEv3
|
|
7
|
+
coder in `compressors`): gcc/cc/clang -std=c99 -O2 -shared -fPIC.
|
|
8
|
+
"""
|
|
9
|
+
import ctypes
|
|
10
|
+
import platform
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from ._core import RANK_NF
|
|
18
|
+
|
|
19
|
+
_HERE = Path(__file__).resolve().parent
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _lib_path():
|
|
23
|
+
system = platform.system()
|
|
24
|
+
if system == 'Windows':
|
|
25
|
+
return _HERE / 'libggdlpc.dll'
|
|
26
|
+
elif system == 'Darwin':
|
|
27
|
+
return _HERE / 'libggdlpc.dylib'
|
|
28
|
+
return _HERE / 'libggdlpc.so'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_lib():
|
|
32
|
+
src = [str(_HERE / 'ggdlpc.c')]
|
|
33
|
+
out = str(_lib_path())
|
|
34
|
+
system = platform.system()
|
|
35
|
+
if system == 'Windows':
|
|
36
|
+
cmd = ['cl.exe', '/O2', '/LD'] + src + [f'/Fe{out}']
|
|
37
|
+
else:
|
|
38
|
+
cc = 'gcc'
|
|
39
|
+
for try_cc in ('gcc', 'cc', 'clang'):
|
|
40
|
+
if shutil.which(try_cc):
|
|
41
|
+
cc = try_cc
|
|
42
|
+
break
|
|
43
|
+
cmd = [cc, '-std=c99', '-O2', '-shared', '-fPIC'] + src + ['-o', out]
|
|
44
|
+
try:
|
|
45
|
+
subprocess.check_call(cmd, cwd=str(_HERE))
|
|
46
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
47
|
+
raise RuntimeError(
|
|
48
|
+
'GGDLPC requires a C compiler (gcc, clang, or cl.exe). '
|
|
49
|
+
'Install one and retry; the library builds automatically on '
|
|
50
|
+
'first import.')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_lib():
|
|
54
|
+
path = _lib_path()
|
|
55
|
+
src_mtime = max((_HERE / n).stat().st_mtime
|
|
56
|
+
for n in ('ggdlpc.c', 'ggdlpc.h'))
|
|
57
|
+
if not path.exists() or path.stat().st_mtime < src_mtime:
|
|
58
|
+
_build_lib()
|
|
59
|
+
return ctypes.CDLL(str(path))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_lib = _load_lib()
|
|
63
|
+
|
|
64
|
+
_lib.glpc_channel_sizeof.restype = ctypes.c_size_t
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _Writer(ctypes.Structure):
|
|
68
|
+
_fields_ = [('buf', ctypes.c_void_p), ('cap', ctypes.c_size_t),
|
|
69
|
+
('acc', ctypes.c_uint64), ('nacc', ctypes.c_uint),
|
|
70
|
+
('nbytes', ctypes.c_size_t)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _Reader(ctypes.Structure):
|
|
74
|
+
_fields_ = [('buf', ctypes.c_void_p), ('nbytes', ctypes.c_size_t),
|
|
75
|
+
('bitpos', ctypes.c_size_t)]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
for name, res, args in [
|
|
79
|
+
('glpc_channel_config', None,
|
|
80
|
+
[ctypes.c_void_p, ctypes.c_int32, ctypes.c_int32, ctypes.c_int32,
|
|
81
|
+
ctypes.c_void_p, ctypes.c_int32, ctypes.c_void_p, ctypes.c_int32,
|
|
82
|
+
ctypes.c_void_p, ctypes.c_int32]),
|
|
83
|
+
('glpc_cell_set', ctypes.c_int,
|
|
84
|
+
[ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p,
|
|
85
|
+
ctypes.c_int, ctypes.c_int32, ctypes.c_int32, ctypes.c_int32,
|
|
86
|
+
ctypes.c_int32, ctypes.c_int32]),
|
|
87
|
+
('glpc_writer_init', None,
|
|
88
|
+
[ctypes.POINTER(_Writer), ctypes.c_void_p, ctypes.c_size_t]),
|
|
89
|
+
('glpc_put_bits', ctypes.c_int,
|
|
90
|
+
[ctypes.POINTER(_Writer), ctypes.c_uint32, ctypes.c_uint]),
|
|
91
|
+
('glpc_flush', ctypes.c_int64, [ctypes.POINTER(_Writer)]),
|
|
92
|
+
('glpc_reader_init', None,
|
|
93
|
+
[ctypes.POINTER(_Reader), ctypes.c_void_p, ctypes.c_size_t]),
|
|
94
|
+
('glpc_get_bits', ctypes.c_int64,
|
|
95
|
+
[ctypes.POINTER(_Reader), ctypes.c_uint]),
|
|
96
|
+
('glpc_encode', ctypes.c_int64,
|
|
97
|
+
[ctypes.POINTER(_Writer), ctypes.c_void_p, ctypes.c_int,
|
|
98
|
+
ctypes.c_void_p, ctypes.c_void_p]),
|
|
99
|
+
('glpc_decode', ctypes.c_int,
|
|
100
|
+
[ctypes.POINTER(_Reader), ctypes.c_void_p, ctypes.c_int,
|
|
101
|
+
ctypes.c_void_p, ctypes.c_void_p]),
|
|
102
|
+
('glpc_maps', ctypes.c_int,
|
|
103
|
+
[ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p,
|
|
104
|
+
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]),
|
|
105
|
+
]:
|
|
106
|
+
fn = getattr(_lib, name)
|
|
107
|
+
fn.restype = res
|
|
108
|
+
fn.argtypes = args
|
|
109
|
+
|
|
110
|
+
_ERR = {-1: 'RANGE', -2: 'CAPACITY', -3: 'TRUNCATED', -4: 'TABLE',
|
|
111
|
+
-5: 'CONFIG'}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _check(ret, what=''):
|
|
115
|
+
if isinstance(ret, int) and ret < 0:
|
|
116
|
+
raise ValueError(f'GGDLPC coder error {_ERR.get(ret, ret)} {what}')
|
|
117
|
+
return ret
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class CChannel:
|
|
121
|
+
"""One channel's C-side coding state, filled from a ChannelBuild."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, build, rank):
|
|
124
|
+
if rank not in RANK_NF:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f'GGDLPC has no C engine for rank {rank}; supported ranks '
|
|
127
|
+
f'are {sorted(RANK_NF)} (1-D, 2-D, 3-D). Higher ranks are '
|
|
128
|
+
'not implemented -- there is no fallback.')
|
|
129
|
+
self.rank = rank
|
|
130
|
+
self.build = build
|
|
131
|
+
self._buf = ctypes.create_string_buffer(_lib.glpc_channel_sizeof())
|
|
132
|
+
edges = np.ascontiguousarray(build.edges, np.int64)
|
|
133
|
+
run_g = np.ascontiguousarray(build.run_g, np.uint8)
|
|
134
|
+
if not build.wq.size:
|
|
135
|
+
raise ValueError('channel has no fitted predictor weights')
|
|
136
|
+
if rank == 1:
|
|
137
|
+
nf = int(build.wq.size)
|
|
138
|
+
if not 1 <= nf <= 9:
|
|
139
|
+
raise ValueError(f'rank-1 FIR order {nf} outside [1, 9]')
|
|
140
|
+
else:
|
|
141
|
+
nf = RANK_NF[rank]
|
|
142
|
+
if build.wq.size != nf:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f'predictor has {build.wq.size} weights but rank {rank} '
|
|
145
|
+
f'needs {nf}')
|
|
146
|
+
wq = np.zeros(9, np.int32)
|
|
147
|
+
wq[:nf] = build.wq
|
|
148
|
+
wq = np.ascontiguousarray(wq)
|
|
149
|
+
_lib.glpc_channel_config(
|
|
150
|
+
self._buf, build.N, build.G, build.stat_cap,
|
|
151
|
+
edges.ctypes.data, build.run_jmax, run_g.ctypes.data,
|
|
152
|
+
nf, wq.ctypes.data, int(build.bq))
|
|
153
|
+
for j, sp in enumerate(build.splits):
|
|
154
|
+
self._set_cell(0, j, sp)
|
|
155
|
+
for j in range(build.run_jmax + 1):
|
|
156
|
+
self._set_cell(1, j, build.excl[j])
|
|
157
|
+
|
|
158
|
+
def _set_cell(self, which, j, sp):
|
|
159
|
+
lens = np.ascontiguousarray(sp.lengths, np.uint8)
|
|
160
|
+
_check(_lib.glpc_cell_set(
|
|
161
|
+
self._buf, which, j, lens.ctypes.data, lens.size,
|
|
162
|
+
sp.win_lo, sp.n_win, sp.k, sp.esc, sp.hi_bits),
|
|
163
|
+
f'cell_set({which},{j})')
|
|
164
|
+
|
|
165
|
+
def maps(self, z):
|
|
166
|
+
"""Encode-side integer maps: (folded symbol, cell id, statistic)."""
|
|
167
|
+
z = np.ascontiguousarray(z, np.int32)
|
|
168
|
+
dims = np.ascontiguousarray(z.shape, np.int64)
|
|
169
|
+
w = np.empty(z.shape, np.int32)
|
|
170
|
+
cell = np.empty(z.shape, np.int32)
|
|
171
|
+
stat = np.empty(z.shape, np.int32)
|
|
172
|
+
_check(_lib.glpc_maps(self._buf, self.rank, z.ctypes.data,
|
|
173
|
+
dims.ctypes.data, w.ctypes.data,
|
|
174
|
+
cell.ctypes.data, stat.ctypes.data), 'maps')
|
|
175
|
+
return w, cell, stat
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class StreamWriter:
|
|
179
|
+
def __init__(self, capacity):
|
|
180
|
+
self._out = np.empty(int(capacity), dtype=np.uint8)
|
|
181
|
+
self._w = _Writer()
|
|
182
|
+
_lib.glpc_writer_init(ctypes.byref(self._w),
|
|
183
|
+
self._out.ctypes.data, self._out.size)
|
|
184
|
+
|
|
185
|
+
def put_bits(self, value, nbits):
|
|
186
|
+
_check(_lib.glpc_put_bits(ctypes.byref(self._w), value, nbits))
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def bitpos(self):
|
|
190
|
+
return int(self._w.nbytes) * 8 + int(self._w.nacc)
|
|
191
|
+
|
|
192
|
+
def encode_channel(self, cchannel, z):
|
|
193
|
+
"""Encode one channel span (raster order). Returns exact bits."""
|
|
194
|
+
z = np.ascontiguousarray(z, np.int32)
|
|
195
|
+
if z.ndim != cchannel.rank:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f'tensor rank {z.ndim} != channel rank {cchannel.rank}')
|
|
198
|
+
dims = np.ascontiguousarray(z.shape, np.int64)
|
|
199
|
+
bits = _lib.glpc_encode(ctypes.byref(self._w), cchannel._buf,
|
|
200
|
+
cchannel.rank, z.ctypes.data,
|
|
201
|
+
dims.ctypes.data)
|
|
202
|
+
return _check(int(bits), 'encode')
|
|
203
|
+
|
|
204
|
+
def flush(self):
|
|
205
|
+
nbytes = _check(int(_lib.glpc_flush(ctypes.byref(self._w))))
|
|
206
|
+
return self._out[:nbytes].tobytes()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class StreamReader:
|
|
210
|
+
def __init__(self, data):
|
|
211
|
+
self._buf = np.frombuffer(data, dtype=np.uint8)
|
|
212
|
+
self._r = _Reader()
|
|
213
|
+
_lib.glpc_reader_init(ctypes.byref(self._r),
|
|
214
|
+
self._buf.ctypes.data, self._buf.size)
|
|
215
|
+
|
|
216
|
+
def get_bits(self, nbits):
|
|
217
|
+
return _check(int(_lib.glpc_get_bits(ctypes.byref(self._r),
|
|
218
|
+
nbits)), 'bits')
|
|
219
|
+
|
|
220
|
+
def decode_channel(self, cchannel, shape):
|
|
221
|
+
"""Sequential mirror of encode_channel; returns the int32 plane."""
|
|
222
|
+
z = np.zeros(shape, np.int32)
|
|
223
|
+
dims = np.ascontiguousarray(z.shape, np.int64)
|
|
224
|
+
_check(_lib.glpc_decode(ctypes.byref(self._r), cchannel._buf,
|
|
225
|
+
cchannel.rank, z.ctypes.data,
|
|
226
|
+
dims.ctypes.data), 'decode')
|
|
227
|
+
return z
|