GGDLPC 0.0.1__py3-none-any.whl
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/__init__.py +28 -0
- GGDLPC/_core.py +159 -0
- GGDLPC/cbridge.py +227 -0
- GGDLPC/codec.py +124 -0
- GGDLPC/fit.py +334 -0
- GGDLPC/ggdlpc.c +644 -0
- GGDLPC/ggdlpc.h +136 -0
- GGDLPC/maps.py +134 -0
- GGDLPC/proxy.py +161 -0
- GGDLPC/reference.py +180 -0
- GGDLPC/selftest.py +141 -0
- GGDLPC/tables.py +168 -0
- ggdlpc-0.0.1.dist-info/METADATA +64 -0
- ggdlpc-0.0.1.dist-info/RECORD +16 -0
- ggdlpc-0.0.1.dist-info/WHEEL +5 -0
- ggdlpc-0.0.1.dist-info/top_level.txt +1 -0
GGDLPC/__init__.py
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
|
+
]
|
GGDLPC/_core.py
ADDED
|
@@ -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
|
GGDLPC/cbridge.py
ADDED
|
@@ -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
|
GGDLPC/codec.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""The GGDLPC codec: blob -> coder; encode/decode of integer tensors.
|
|
2
|
+
|
|
3
|
+
The primitive: one call encodes one integer tensor (C channels, rank 1/2/3
|
|
4
|
+
spatial dims, values in [-N, N]) as ONE self-contained bitstream --
|
|
5
|
+
byte-padded at its end, decodable from its own bytes alone, truncatable at
|
|
6
|
+
any channel boundary (channel prefixes are literal bit prefixes; recorded
|
|
7
|
+
per-channel bit counts locate them). C = 1 gives the single-channel call;
|
|
8
|
+
everything larger (macroregions, mixed-resolution scale groups, operating
|
|
9
|
+
points, files) is caller composition via the stream-level API
|
|
10
|
+
(stream_writer / encode_channel / stream_reader / decode_channel), which
|
|
11
|
+
shares one byte pad across the composition.
|
|
12
|
+
|
|
13
|
+
The persistent model is the blob: six scalars per channel (mu; scale law
|
|
14
|
+
a, b; shape law u, v) plus fixed-point predictor weights. Edges, split
|
|
15
|
+
config, canonical Huffman tables, and the run mode are generated from the
|
|
16
|
+
scalars at construction (tables.py). No backward compatibility with the
|
|
17
|
+
FRAPPEv3 lawcoder blob or streams.
|
|
18
|
+
"""
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from ._core import RANK_NF
|
|
22
|
+
from .cbridge import CChannel, StreamReader, StreamWriter
|
|
23
|
+
from .tables import build_channel
|
|
24
|
+
|
|
25
|
+
SPEC = ('GGDLPC v1: unified coder (one learned causal FIR predictor per '
|
|
26
|
+
'channel with arithmetic-mean fallback; one stateless pooled '
|
|
27
|
+
'statistic: mean of the causal template abs differences and the '
|
|
28
|
+
'last 16 coded abs residuals in scan order; two per-channel '
|
|
29
|
+
'affine laws -> analytic folded GGD '
|
|
30
|
+
'conditionals on the tau=2^(1/4) scale grid; canonical Huffman '
|
|
31
|
+
'tables under the derived quotient split, <= 63 windowed hi '
|
|
32
|
+
'values + escape at any bit depth; derived zero-run mode with '
|
|
33
|
+
'elementary Golomb codes and exclusion tables, always available); '
|
|
34
|
+
'inputs in [-N, N], N <= 65536; one call = one byte-padded '
|
|
35
|
+
'bitstream, channel prefixes are literal bit prefixes')
|
|
36
|
+
VERSION = 1
|
|
37
|
+
|
|
38
|
+
# Worst-case bits per coded sample: <= 63 (codeword) + 18 (escape hi)
|
|
39
|
+
# + 18 (lo) < 128 -> 16 bytes/sample capacity is always safe.
|
|
40
|
+
_CAP_PER_SAMPLE = 16
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def new_blob(N, rank, channels, provenance=None, K_beta=None):
|
|
44
|
+
"""Assemble a JSON-able codec blob from fit results."""
|
|
45
|
+
return {'spec': SPEC, 'version': VERSION, 'N': int(N),
|
|
46
|
+
'rank': int(rank), 'K_beta': K_beta,
|
|
47
|
+
'channels': list(channels),
|
|
48
|
+
'provenance': provenance or {}}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Codec:
|
|
52
|
+
def __init__(self, blob):
|
|
53
|
+
assert blob.get('spec') == SPEC, \
|
|
54
|
+
'GGDLPC blob spec mismatch (no back-compat with other coders)'
|
|
55
|
+
assert blob['version'] == VERSION
|
|
56
|
+
self.blob = blob
|
|
57
|
+
self.N = int(blob['N'])
|
|
58
|
+
self.rank = int(blob['rank'])
|
|
59
|
+
if self.rank not in RANK_NF:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f'GGDLPC supports ranks {sorted(RANK_NF)}; got {self.rank}')
|
|
62
|
+
if not (1 <= self.N <= 65536):
|
|
63
|
+
raise ValueError(f'N must be in [1, 65536]; got {self.N}')
|
|
64
|
+
self.n_channels = len(blob['channels'])
|
|
65
|
+
self.builds = [build_channel(ch, self.N)
|
|
66
|
+
for ch in blob['channels']]
|
|
67
|
+
self.cchannels = [CChannel(b, self.rank) for b in self.builds]
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------ the primitive
|
|
70
|
+
|
|
71
|
+
def encode(self, z):
|
|
72
|
+
"""(C, *S) integer tensor -> (bytes, per-channel bit counts).
|
|
73
|
+
|
|
74
|
+
One self-contained byte-padded stream; channel prefixes are
|
|
75
|
+
literal bit prefixes located by the returned counts."""
|
|
76
|
+
z = np.asarray(z)
|
|
77
|
+
if z.ndim != self.rank + 1:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f'expected a (C, *S) tensor of rank {self.rank + 1}, '
|
|
80
|
+
f'got shape {z.shape}')
|
|
81
|
+
C = z.shape[0]
|
|
82
|
+
if C > self.n_channels:
|
|
83
|
+
raise ValueError(f'{C} channels but blob has {self.n_channels}')
|
|
84
|
+
w = self.stream_writer(_CAP_PER_SAMPLE * z.size + 64)
|
|
85
|
+
ch_bits = np.zeros(C, np.int64)
|
|
86
|
+
for c in range(C):
|
|
87
|
+
ch_bits[c] = self.encode_channel(w, z[c], c)
|
|
88
|
+
data = w.flush()
|
|
89
|
+
assert len(data) == (int(ch_bits.sum()) + 7) // 8
|
|
90
|
+
return data, ch_bits
|
|
91
|
+
|
|
92
|
+
def decode(self, data, shape, n_channels=None):
|
|
93
|
+
"""Decode (a prefix of) an encode() stream. shape: the full
|
|
94
|
+
(C, *S); n_channels: decode only the first n channels."""
|
|
95
|
+
C = shape[0]
|
|
96
|
+
n = C if n_channels is None else min(n_channels, C)
|
|
97
|
+
r = self.stream_reader(data)
|
|
98
|
+
out = np.zeros(shape, np.int32)
|
|
99
|
+
for c in range(n):
|
|
100
|
+
out[c] = self.decode_channel(r, shape[1:], c)
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------- composition (shared pad)
|
|
104
|
+
|
|
105
|
+
def stream_writer(self, capacity):
|
|
106
|
+
return StreamWriter(capacity)
|
|
107
|
+
|
|
108
|
+
def stream_reader(self, data):
|
|
109
|
+
return StreamReader(data)
|
|
110
|
+
|
|
111
|
+
def capacity_for(self, n_samples):
|
|
112
|
+
return _CAP_PER_SAMPLE * int(n_samples) + 64
|
|
113
|
+
|
|
114
|
+
def encode_channel(self, writer, plane, channel):
|
|
115
|
+
"""Code one channel span (rank-`rank` tensor) into an open
|
|
116
|
+
writer. Returns the exact bit count."""
|
|
117
|
+
return writer.encode_channel(self.cchannels[channel], plane)
|
|
118
|
+
|
|
119
|
+
def decode_channel(self, reader, shape, channel):
|
|
120
|
+
return reader.decode_channel(self.cchannels[channel], shape)
|
|
121
|
+
|
|
122
|
+
def maps(self, plane, channel):
|
|
123
|
+
"""Encode-side integer maps (folded symbol, cell, statistic)."""
|
|
124
|
+
return self.cchannels[channel].maps(plane)
|