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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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