meridionalissoftware/hscq

Hopf Spherical Compression (& Quantization) in C++23

2

stars

3

commits

C++

primary language

Aug 20, 2026

updated

README

hsc

Hopf Spherical Compression (& Quantization)

... is a header-only C++23 fixed-rate lossy compression library built on the micron corelib, inspired in part via Constructive Spherical Codes by Hopf Foliations (SCHF; Miyamoto, Costa, Sá Earp, IEEE Trans. Inf. Theory 2021).

The sphere S^(2n−1) foliates into leaves (S^(n−1) × S^(n−1))_η, recursively down to S³, which foliates into flat tori carrying points on internal circles. A data block becomes Hopf coordinates, then quantized and finally reduced to a single integer index.

Encoding and decoding are (O(n log n)) with no stored codebook: both sides deterministically derive the same integer skeleton tables from the single fixed-point distance parameter stored in the stream header.

Records have constant width, making the payload randomly accessible by block number. The compression ratio is also guaranteed for every input, including incompressible noise, where entropy-based compressors must actually expand the data.

#include <hsc/hsc.hpp>

micron::vector<u8> data = ...;
hsc::fhsc z = hsc::hopf(data, hsc::hopf_opts{ .level = 6, .dim_log2 = 3 });   // any bytes; cannot fail
auto back  = hsc::unhopf(z);                                                  // result<fhsc>

// typed float lanes
auto zv = hsc::hopf(my_f32_vector, hsc::hopf_opts{ .m = hsc::mode::vec });    // shape-gain f32 blocks
auto zu = hsc::hopf(embeddings,   hsc::hopf_opts{ .m = hsc::mode::unit });    // unit-norm: no gain field
auto zq = hsc::hopf(states,       hsc::hopf_opts{ .m = hsc::mode::quotient });// phase-invariant complex pairs
auto z4 = hsc::hopf(poses,        hsc::hopf_opts{ .m = hsc::mode::quat });    // gauge-invariant quaternion pairs
auto z8 = hsc::hopf(pairs16,      hsc::hopf_opts{ .m = hsc::mode::oct });     // octonion pairs (the last fibration)

// compile time, same wire bytes
static constexpr hsc::ct::str body{ "hopfed at compile time" };
constexpr auto stream = hsc::ct::hopf<body>();
constexpr auto plain  = hsc::ct::unhopf<stream>();

[!WARNING] hsc has been tested exclusively on amd64 (AVX2+) for now.

[!WARNING] hsc is currently Linux only, by nature of micron being Linux only. No other kernels or operating systems are supported. BSDs may work, but this is not guaranteed.

Installation

hsc depends only on the micron corelib, specifically micron must be reachable in source as #include <micron/...>. To build the in-tree tests, benches or examples, first run cd include/ && sh fetch_micron.sh, which fetches a local copy of micron to the include/ directory — this step is required: every shipped build file (*.duck and *.sh alike) resolves micron at include/micron and compiles its start/ runtime from there. For your own projects you can instead do git clone --depth=1 --branch 1.9.11-lts https://github.com/rfgplk/micron.cpp.git and install it to your /usr/include via its install scripts.

hsc by default uses duck as the build tool, which is optional and not at all required. The *.sh scripts are portable variants of the *.duck scripts with one caveat: they build without duck's default -fstack-protector-all and -flto=8 (they pass -fno-stack-protector), which is the configuration the published perf numbers do not use — results from the *.sh builds are correct but not comparable to the perf tables.

Generating BENCHMARKS.md

BENCHMARKS.md is already pregenerated and commited along with the rest of the source code. If you would like to regenerate it, first set up the working environment of the library, as described in Installation, and follow the following steps:

Build the reference corpus first:

python3 scripts/corpus.py fetch && python3 scripts/corpus.py models

Then generate the report:

python3 scripts/ratio_report.py > BENCHMARKS.md

Mathematics

hsc is a vector quantizer with no codebook. Everything stream-defining is derived, on both sides, from one integer: d_q = round(d · 2^24), the fixed-point minimum codeword distance.

Shape and gain

A block of n = 2^dim_log2 values (n ∈ {4, 8, 16, 32, 64}) is split into a magnitude and a direction:

g = ‖v‖₂                         x̂ = v / g  ∈ S^(n−1)
step = scale / (2^b − 1)         q  = clamp(round(g / step), 0, 2^b − 1)
v̂ = q · step · x̂                 q = 0 reconstructs exact zeros — and is reachable: any
                                 block with g < step/2 rounds to it (the gain cliff below)

b is gain_bits (default 8). For bin, bytes are centered on 127.5 first, so scale = 127.5·√n is the exact maximum norm of such a block. unit and the quotient family (quotient, quat, oct) carry no gain field at all. The direction is what the sphere layer codes, and it is where all the work is.

Generalized Hopf foliations

Every y ∈ S^(n−1) decomposes along a half-dimension split as

y = (cos η · u,  sin η · v)      u, v ∈ S^(n/2−1),  η ∈ [0, π/2]

so the sphere foliates into leaves (S^(n/2−1) × S^(n/2−1))_η indexed by the energy-split angle η. hsc discretizes η into a fan centered on the equal-energy split π/4:

Δη = 2·asin(d/2)        t = ⌊π / (4·asin(d/2))⌋        half = ⌊t/2⌋
η_i = π/4 + i·Δη        i ∈ [−half, +half]             leaves = 2·half + 1

Two codewords in the same leaf, differing only in their u factor, are separated by cos η · (their separation in S^(n/2−1)). So to keep the whole code ≥ d apart, the child code in the u factor must be built at distance d / cos η, and the one in v at d / sin η. That is the recursion: the same construction, one dimension down, at a leaf-dependent distance.

Distance grid and why the tables stay finite

Left alone, the set of distinct (dim, d′) skeletons multiplies by ~t per split, on the order of 10⁸ tables at dim 64. hsc snaps every child distance up onto a fixed geometric grid:

grid[g] = d_q( 2 · (16303/16384)^g )       g = 0 .. 2047,  spanning [7.8e-5, 2]

Snapping up is what preserves the guarantee: a child built at d″ ≥ d/cos η and then scaled by cos η is still ≥ d apart. And it collapses the reachable set to at most one node per grid member per level, which is what makes the memo finite and the skeleton buildable at all. 16303/16384 = 16303/2^14 is dyadic, hence exactly representable, so the grid comes out bit-identical at consteval and at runtime. Changing these constants is a format break.

Cardinality

M(n, d) = Σ_i  M(n/2, snap(d/cos η_i)) · M(n/2, snap(d/sin η_i))

with the 4D base case below. Note that M is genuinely large (log₂ M ≈ 147.7 at dim 64 level 7, i.e. 148 shape bits) therefore it's carried in micron::arbuint<1024>; only M mod 2^64 is kept in the skeleton nodes.

4D base case

S³ foliates into flat tori T_η = S¹_{cos η} × S¹_{sin η}, each carrying n internal circles of m equidistant points, consecutive circles shifted by half a step. (This n is the code's circle count, following the paper and s3.hpp; it is not the block dimension.)

m  = ⌊ π / asin( d / (2 cos η) ) ⌋      — m = 1 when d > 2 cos η
n₂ = ⌊ 2π / asin( d / (2 sin η) ) ⌋     — n = 1 outright when d > 2 sin η
arg = d²/(4 sin²η) − (cos²η / sin²η) · sin²(π / 2m)
n₁ = ⌊ π / asin(√arg) ⌋   when 0 < arg ≤ 1;  n₁ = 1 when √arg > 1;  else n₁ = n₂
n  = max( 2·⌊ min(n₁, n₂) / 2 ⌋ , 1 )   — forced even (so the half-step shift closes)
                                          except the single-circle floor n = 1
M(4, d) = Σ_x m_x · n_x

Note that at d = 2 the whole torus collapses to m = n = 1 and M(4, 2) = 1; exactly what tests/s3.cpp pins at dq_max.

A codeword is

Δξ₁ = 2π/m      ξ₁ = j·Δξ₁ + k·Δξ₁/2
Δξ₂ = 2π/n      ξ₂ = k·Δξ₂
x = ( cos η cos ξ₁,  cos η sin ξ₁,  sin η cos ξ₂,  sin η sin ξ₂ )

Quantizing

Encoding is a descent with no codebook:

η = atan2( √Σ_{c ≥ h} y_c² ,  √Σ_{c < h} y_c² )        h = n/2
i = clamp( round((η − π/4) / Δη), −half, +half )
                                        … then recurse into both halves

and decoding is the same walk in reverse, scaling the two halves by cos η_x and sin η_x.

Index

Per-node fields collapse into one integer by mixed radix, using arbint prefix sums over the leaf fan:

row_off[x] = Σ_{x′ < x} M(c₁(x′)) · M(c₂(x′))
a          = row_off[x] + a₂ · M(c₁) + a₁

Unranking inverts it with a binary search over the monotone prefix sums plus one divmod. Both sides rebuild row_off and M from d_q alone, so the "codebook" is a pure function of the header.

Rate identity

block_elems = n        (the quotient family pins n: quotient 4, quat 8, oct 16)
shape_bits  = ⌈log₂ M⌉ = bit_length(M − 1)
record_bits = (bin|vec ? gain_bits : 0) + shape_bits
stream      = 40 + ⌈ nblocks · record_bits / 8 ⌉ + 8
bits/elem   = record_bits / block_elems

Closed form in (mode, dim, d_q, gain_bits, n_elems) and in nothing about the data — the same for /dev/urandom and for JSON. hsc::rate() returns it without running the codec.

M == 1 gives shape_bits == 0: the shape field has zero width, the record carries the gain alone, and the block reconstructs to one fixed direction times a scalar. That is data loss wearing a compression ratio, and hsc::degenerate() is the query for it. At d ≥ 1 every dim ≥ 8 collapses to it, and dim ≥ 16 is already there at level 1.

Distortion via packing

SCHF guarantees that codewords are ≥ d apart. It says nothing about the distance from an arbitrary input to the nearest codeword, and uneven energy splits clamp to the edge of the leaf fan and stack down the recursion. So there is no derived distortion bound, only a fitted law:

rmse ≈ min( κ(n) · d · σ ,  √2 · σ )        σ = RMS of the centered input
κ(n) = 0.55, 0.81, 1.15, 1.70, 2.35  at n = 4, 8, 16, 32, 64   —  tracking 0.29·√n

√2·σ is the no-information level: error and signal uncorrelated. The fit holds to a pooled median relative error of 0.09 wherever a cell is shippable (PSNR ≥ 20 dB) — per-corpus medians run 0.05–0.14 on the ten non-flat corpora. On flat data it over-predicts badly (median ~0.87 on the runs corpus: a constant run centers onto (1..1)/√n, i.e. η = π/4 exactly — the densest leaf) and it under-predicts as a cell approaches saturation. Those domains are reported separately in BENCHMARKS.md rather than averaged into one flattering number.

Compression by symmetry (quotient)

Read a 4-float block as (z₀, z₁) = (y₀ + i y₁, y₂ + i y₃) ∈ S³. If the data is insensitive to a global phase q ~ q·e^{iψ}, the fiber coordinate is pure redundancy. The Hopf map

h(z₀, z₁) = ( 2 z₀ z̄₁ ,  |z₀|² − |z₁|² )        S³ → S² = S³/S¹

projects onto the quotient, and only the class is stored. h(λq) = λ²·h(q), so dividing by ‖q‖² normalizes with no square root anywhere. S² gets a latitude-band code:

Δθ = 2·asin(d/2)     T = ⌊π/Δθ⌋ + 1     θ₀ = (π − (T−1)Δθ)/2
m_b = ⌊ π / asin( d / (2 sin θ_b) ) ⌋   (m_b = 1 when d > 2 sin θ_b)     M = Σ_b m_b

which is exactly log₂(1/d)-ish bits cheaper than the full S³ code at the same d — measured 2 bits/block at level 3 growing to 4 at level 7. Decoding returns the canonical section z₀ = √((1+p₂)/2) real ≥ 0, z₁ = (p₀ − i p₁)/(2 z₀).

The full Hopf family (quat, oct)

The Hopf family of sphere-by-sphere fibrations has exactly four members; the three with positive-dimensional fiber are the ones a codec can exploit, and hsc implements all of them:

mode::quotient: S³ → S² = S³/S¹ mode::quat: S⁷ → S⁴ = S⁷/S³ mode::oct: S¹⁵ → S⁸ = S¹⁵/S⁷

(The fourth is the real double cover S¹ → S¹ with fiber S⁰ — a zero-dimensional fiber saves no bits.) The Hopf Invariant One Theorem (Adams' theorem: maps of Hopf invariant one exist only for n ∈ {1, 2, 4, 8}) rules out any fibration of a sphere by spheres beyond these four.

All three use the same map shape:

h(x, y) = ( 2 x ȳ ,  |x|² − |y|² )         normalized by ‖(x,y)‖², scale-invariant

The base retains the projective point [x : y]; the fiber is the discarded symmetry. For quaternions, this is exact invariance under simultaneous right multiplication:

(q₀, q₁) → (q₀g, q₁g)

Left multiplication is not quotiented and rotates the base. Octonions have no corresponding group action because multiplication is non-associative; their fibers are the graph spheres { (a, q·a) : |a| = 1 }

where q = y·x⁻¹.

Component convention: Our codec uses vector-first, scalar-last components

quaternion: (x, y, z, w),

octonion: (e₁..e₇, e₀) with e₁,e₂,e₃ = i,j,k, e₄ = ℓ, e₅,e₆,e₇ = iℓ,jℓ,kℓ (Cayley–Dickson construction).

Pairs are adjacent: f[0..n-1], f[n..2n-1]. This matches pose-data memory layouts; quotient's frozen (re, im) scalar-first reading does not change.

The bases S⁴ ⊂ ℝ⁵ and S⁸ ⊂ ℝ⁹ get a cap-anchored, equator-anchored suspension code (sphere/susp.hpp), different from the S² band layout above. The poles are exact codewords ([x:0] and [0:y] classes, spinor basis states) and the interior band count is odd with the equator a band center.

Measured fiber savings vs unit at the same d (level 6): quat 22 → 15 bits/block (7 saved), oct 37 → 24 (13 saved), growing to 10 and 20 at level 7. The quat index stays u64 end to end (M = 2^58.06 at the d = 1e-4 floor, measured); the oct index crosses u64 at level 11 (M ≈ 2^66.4 there; level 10, at 59 bits, is the last that fits) and rides the arbint pack lane. Neither mode can go degenerate: even at d = 2 the two poles remain (M = 2).

Pre-rotation

.transform = true (wire flags bit 2) applies

y = (1/√n) · H · D · x        H = Walsh–Hadamard,  D = pinned ±1 diagonal

H = Hᵀ and H² = nI, so (1/√n)H is orthogonal and involutive. Every basis vector maps to an exact 50/50 half-energy split at every recursion level: η = π/4, the densest leaf, making one-hot rows and axis spikes codable. It costs no rate at all, but smooth data prefers the raw basis, so it is off by default, and it is excluded from the whole quotient family, whose Hopf maps need the algebra structure a generic rotation breaks. The butterfly is adds and subs only (no fma); contractions on one side and not the other would break consteval/runtime bit-identity.

Determinism

Both sides rebuild every skeleton from d_q, never from a raw f64. One ulp of drift in asin would move a ⌊⌋ and change M, so every stream carries skel_guard = M mod 2^64: a drifted decoder fails bad_skeleton instead of quietly emitting wrong floats. tests/comptime.cpp proves consteval streams byte-identical to runtime streams, and tests/exact.cpp pins every SIMD kernel against its scalar twin — that equality is the format contract.

Rate and distortion, cell by cell

The rates are exact and data-independent: hsc::rate() returns it without reading a byte or running the codec, and scripts/compression_ratio_model.py reproduces every cell in independent Python. Distortions (rmse/psnr) below are measured on one named corpus, mixed (unstructured heterogeneous, 512 KiB), in bin mode. Per-corpus grids for all eleven corpora are in BENCHMARKS.md. PSNR is 20·log10(255/rmse) over 8-bit full scale.

d is the nominal minimum codeword distance of the level preset; the stream carries d_q = round(d·2^24). bits/elem is record_bits / n — bits per input byte in bin mode, bits per weight in the f32 lanes — so the two ratio columns are the same number read against an 8-bit and a 32-bit source. All rate columns here are payload only (record bits, no frame): a real stream adds a flat 48 bytes (40-byte header + 8-byte trailer), which is why BENCHMARKS.md's measured 512 KiB ratios read a hair lower (5.56x vs 5.57x at dim64 L6). Both ratio columns are computed from the exact record_bits / n, not from the rounded bits/w shown.

dimLdshape bitsrecord bitsbits/wvs bytesvs fp32rmsepsnr
41.94123.002.67x10.67x38.5516.41
42.76143.502.29x9.14x21.8521.34
43.58164.002.00x8.00x16.2223.93
44.49174.251.88x7.53x13.9025.27
45.310184.501.78x7.11x9.7928.31
46.212205.001.60x6.40x6.1532.35
47.115235.751.39x5.57x2.9938.62
48.0518266.501.23x4.92x1.5444.36
49.0222307.501.07x4.27x0.6651.80
410.0125338.250.97x3.88x0.3656.94
411.00528369.000.89x3.56x0.1465.12
412.002324010.000.80x3.20x0.0098.34
413.001354310.750.74x2.98x0.0099.99
4145e-4384611.500.70x2.78x0.0099.99
81.96141.754.57x18.29x48.5714.40
82.79172.133.76x15.06x38.2816.47
83.512202.503.20x12.80x28.3619.08
84.414222.752.91x11.64x20.8921.73
85.317253.132.56x10.24x16.9523.55
86.222303.752.13x8.53x9.8828.24
87.129374.631.73x6.92x4.7534.59
88.0536445.501.45x5.82x2.3640.67
89.0245536.631.21x4.83x0.9948.19
810.0152607.501.07x4.27x0.5553.32
811.00559678.380.96x3.82x0.2659.67
812.00269779.630.83x3.32x0.0869.64
161.9080.50— †— †102.927.88
162.713211.316.10x24.38x49.1214.31
163.516241.505.33x21.33x42.2715.61
164.421291.814.41x17.66x35.4717.13
165.328362.253.56x14.22x22.4621.10
166.237452.812.84x11.38x15.6924.22
167.152603.752.13x8.53x7.1331.06
168.0568764.751.68x6.74x3.4737.32
169.0288966.001.33x5.33x1.4245.11
1610.011031116.941.15x4.61x0.7650.52
1611.0051181267.881.02x4.06x0.4255.71
321.9080.25— †— †102.997.87
322.714220.6911.64x46.55x102.517.92
323.519270.849.48x37.93x98.498.26
324.429371.166.92x27.68x45.5514.96
325.339471.475.45x21.79x36.2616.94
326.257652.033.94x15.75x23.4220.74
327.190983.062.61x10.45x10.8127.45
328.051221304.061.97x7.88x5.0834.01
329.021641725.381.49x5.95x2.0242.01
3210.011952036.341.26x5.04x1.0547.73
641.9080.13— †— †102.987.88
642.715230.3622.26x89.04x102.987.88
643.521290.4517.66x70.62x102.847.89
644.432400.6312.80x51.20x100.658.07
645.358661.037.76x31.03x47.2914.64
646.284921.445.57x22.26x34.6817.33
647.11481562.443.28x13.13x16.2123.94
648.052142223.472.31x9.23x7.3530.80
649.022993074.801.67x6.67x2.8738.96

Degenerate, not compression. shape_bits == 0 means the recursion collapsed to M == 1: the shape field has zero width, the record is the gain alone, and the direction is gone. The apparent 16x/32x/64x is pure data loss — the measured RMSE sits at the √2·σ no-information level — so no ratio is quoted for those rows. hsc::degenerate() is the query, and the quant.hpp porcelain refuses such a plan unless you type out allow_degenerate. The high-ratio region that is real is the dim axis: dim32 L2 = 11.6x, dim64 L2 = 22.3x, dim64 L3 = 17.7x, at honest (if large) distortion.

Two readings worth taking from the table. Down a column, dimension buys rate: the same d = 0.2 costs 5.00 bits/elem at dim 4 and 1.44 at dim 64. Across a row, d buys accuracy at roughly rmse ∝ d. The two are the whole trade-off surface, and hsc::plan_for() walks it for you.

Bit-exact bytes. The psnr 99.99 in the two finest dim-4 rows is not "very good" — the bench clamps psnr to 99.99 below rmse 1e-9, and an integer rmse is either 0 or ≥ 1/√n, so those cells are exact. mode::bin reconstructs integers, so once the worst per-element error falls under half a count the bytes round back to themselves. hsc::opts_exact_bytes names that cell (dim 4, L13, gain_bits 8) and hsc::exact_bytes(o) is the predicate; at compile time hsc::ct::exact<Src, Back>() proves it for one payload, which is what examples/07_comptime.cpp does. Read the price honestly: 10.75 bits/byte, ratio 0.744x — that is expansion, and it must never be quoted as compression. Note also which axis got you there: level up, dim down. Since kappa(dim) ≈ 0.29·√dim, a wider block is less accurate at the same d, and dim 4 is the only lane where the fine end of the ladder is reachable inside hsc::ct's fixed comptime arenas (dim 8 caps at L11, dim 16 at L10, dim 32/64 at L8). hsc::bin_exact_level(dim_log2) answers 0 for every other dim, meaning unmeasured, not no: the corpus sweep stops at d8 L12, d16 L11, d32 L10, d64 L9.

Preset ladder

quant.hpp provides for seven predefined quantization defaults: one cell per dim (plus q_balanced), each Pareto-optimal within its own dim column. The ladder is not a Pareto frontier over the whole dim × {L5, L6, L7} grid — dim8 L7 (4.625 b/w, est 0.081) strictly dominates q_finest, and dim16 L7 matches q_fine's rate at lower estimated error; reach those cells through hsc::plan_for().

presetdimLdbits/weightvs fp32est rel-rmsemeasured rel-rmsepsnr dB
q_min645.31.0312531.03x0.7050.71029.28
q_tiny646.21.437522.26x0.4700.56131.33
q_small326.22.0312515.75x0.3400.37434.86
q_compact166.22.812511.38x0.2300.24638.49
q_balanced327.13.062510.45x0.1700.17141.64
q_fine86.23.758.53x0.1620.16142.17
q_finest46.25.06.40x0.1100.10046.28

est rel-rmse is the fitted law at that cell, available at compile time from the shape alone. measured is a real round-trip of the first 4096 rows of all-MiniLM-L6-v2's embeddings.word_embeddings.weight (384 wide) through mode::vec; PSNR there is 20·log10(peak/rmse) against the tensor's own peak magnitude, which is not the 8-bit-full-scale PSNR of the table above. Rates shown are the payload rate the constants state; the frame is a flat 48 bytes per stream.

For context, on that same tensor int4 costs 4 bits/weight at rel-RMSE 0.140 and int2 costs 2 at 0.837 (int-N billed at exactly N by the usual convention — the per-row f16 scale, 16/cols, is not counted), while zstd-19 — lossless, so no error at all — manages 1.19x. The full chart, with the lossless coders and int-N in one table, is the opening section of BENCHMARKS.md.

Model and vector quantization

A tensor is not a flat run of elements, so the codec verbs above are the wrong altitude for one. quant.hpp is the porcelain that knows about rows, bits per weight and the lanes that are wrong for a weight matrix:

hsc::tensor t = hsc::tensor::of(weights, 384);            // [n, 384] row-major f32

auto q = hsc::quantize(t, { .bits_per_weight = 3.0 });    // result<qstream>: bytes + the plan
auto e = hsc::measure(t, hsc::floats{...});               // rel-rmse / cosine / psnr

auto blob = hsc::pack(t, { .rel_rmse = 0.2 });            // self-describing HSCQ container
hsc::qreader rd{ blob_view, sc };
rd.row(9182, out);                                        // one vector, O(cols), no table decode

Build/test with duck: duck batch test.duck · benches duck batch build.duck · examples duck batch examples.duck · comptime stress scripts/ctbuild. Never -Ofast (floating-point codec path). MIT license.

Contributors

rfgplk

3 commits

meridionalissoftware/hscq

Hopf Spherical Compression (& Quantization) in C++23

2

stars

3

commits

C++

primary language

Aug 20, 2026

updated

README

hsc

Hopf Spherical Compression (& Quantization)

... is a header-only C++23 fixed-rate lossy compression library built on the micron corelib, inspired in part via Constructive Spherical Codes by Hopf Foliations (SCHF; Miyamoto, Costa, Sá Earp, IEEE Trans. Inf. Theory 2021).

The sphere S^(2n−1) foliates into leaves (S^(n−1) × S^(n−1))_η, recursively down to S³, which foliates into flat tori carrying points on internal circles. A data block becomes Hopf coordinates, then quantized and finally reduced to a single integer index.

Encoding and decoding are (O(n log n)) with no stored codebook: both sides deterministically derive the same integer skeleton tables from the single fixed-point distance parameter stored in the stream header.

Records have constant width, making the payload randomly accessible by block number. The compression ratio is also guaranteed for every input, including incompressible noise, where entropy-based compressors must actually expand the data.

#include <hsc/hsc.hpp>

micron::vector<u8> data = ...;
hsc::fhsc z = hsc::hopf(data, hsc::hopf_opts{ .level = 6, .dim_log2 = 3 });   // any bytes; cannot fail
auto back  = hsc::unhopf(z);                                                  // result<fhsc>

// typed float lanes
auto zv = hsc::hopf(my_f32_vector, hsc::hopf_opts{ .m = hsc::mode::vec });    // shape-gain f32 blocks
auto zu = hsc::hopf(embeddings,   hsc::hopf_opts{ .m = hsc::mode::unit });    // unit-norm: no gain field
auto zq = hsc::hopf(states,       hsc::hopf_opts{ .m = hsc::mode::quotient });// phase-invariant complex pairs
auto z4 = hsc::hopf(poses,        hsc::hopf_opts{ .m = hsc::mode::quat });    // gauge-invariant quaternion pairs
auto z8 = hsc::hopf(pairs16,      hsc::hopf_opts{ .m = hsc::mode::oct });     // octonion pairs (the last fibration)

// compile time, same wire bytes
static constexpr hsc::ct::str body{ "hopfed at compile time" };
constexpr auto stream = hsc::ct::hopf<body>();
constexpr auto plain  = hsc::ct::unhopf<stream>();

[!WARNING] hsc has been tested exclusively on amd64 (AVX2+) for now.

[!WARNING] hsc is currently Linux only, by nature of micron being Linux only. No other kernels or operating systems are supported. BSDs may work, but this is not guaranteed.

Installation

hsc depends only on the micron corelib, specifically micron must be reachable in source as #include <micron/...>. To build the in-tree tests, benches or examples, first run cd include/ && sh fetch_micron.sh, which fetches a local copy of micron to the include/ directory — this step is required: every shipped build file (*.duck and *.sh alike) resolves micron at include/micron and compiles its start/ runtime from there. For your own projects you can instead do git clone --depth=1 --branch 1.9.11-lts https://github.com/rfgplk/micron.cpp.git and install it to your /usr/include via its install scripts.

hsc by default uses duck as the build tool, which is optional and not at all required. The *.sh scripts are portable variants of the *.duck scripts with one caveat: they build without duck's default -fstack-protector-all and -flto=8 (they pass -fno-stack-protector), which is the configuration the published perf numbers do not use — results from the *.sh builds are correct but not comparable to the perf tables.

Generating BENCHMARKS.md

BENCHMARKS.md is already pregenerated and commited along with the rest of the source code. If you would like to regenerate it, first set up the working environment of the library, as described in Installation, and follow the following steps:

Build the reference corpus first:

python3 scripts/corpus.py fetch && python3 scripts/corpus.py models

Then generate the report:

python3 scripts/ratio_report.py > BENCHMARKS.md

Mathematics

hsc is a vector quantizer with no codebook. Everything stream-defining is derived, on both sides, from one integer: d_q = round(d · 2^24), the fixed-point minimum codeword distance.

Shape and gain

A block of n = 2^dim_log2 values (n ∈ {4, 8, 16, 32, 64}) is split into a magnitude and a direction:

g = ‖v‖₂                         x̂ = v / g  ∈ S^(n−1)
step = scale / (2^b − 1)         q  = clamp(round(g / step), 0, 2^b − 1)
v̂ = q · step · x̂                 q = 0 reconstructs exact zeros — and is reachable: any
                                 block with g < step/2 rounds to it (the gain cliff below)

b is gain_bits (default 8). For bin, bytes are centered on 127.5 first, so scale = 127.5·√n is the exact maximum norm of such a block. unit and the quotient family (quotient, quat, oct) carry no gain field at all. The direction is what the sphere layer codes, and it is where all the work is.

Generalized Hopf foliations

Every y ∈ S^(n−1) decomposes along a half-dimension split as

y = (cos η · u,  sin η · v)      u, v ∈ S^(n/2−1),  η ∈ [0, π/2]

so the sphere foliates into leaves (S^(n/2−1) × S^(n/2−1))_η indexed by the energy-split angle η. hsc discretizes η into a fan centered on the equal-energy split π/4:

Δη = 2·asin(d/2)        t = ⌊π / (4·asin(d/2))⌋        half = ⌊t/2⌋
η_i = π/4 + i·Δη        i ∈ [−half, +half]             leaves = 2·half + 1

Two codewords in the same leaf, differing only in their u factor, are separated by cos η · (their separation in S^(n/2−1)). So to keep the whole code ≥ d apart, the child code in the u factor must be built at distance d / cos η, and the one in v at d / sin η. That is the recursion: the same construction, one dimension down, at a leaf-dependent distance.

Distance grid and why the tables stay finite

Left alone, the set of distinct (dim, d′) skeletons multiplies by ~t per split, on the order of 10⁸ tables at dim 64. hsc snaps every child distance up onto a fixed geometric grid:

grid[g] = d_q( 2 · (16303/16384)^g )       g = 0 .. 2047,  spanning [7.8e-5, 2]

Snapping up is what preserves the guarantee: a child built at d″ ≥ d/cos η and then scaled by cos η is still ≥ d apart. And it collapses the reachable set to at most one node per grid member per level, which is what makes the memo finite and the skeleton buildable at all. 16303/16384 = 16303/2^14 is dyadic, hence exactly representable, so the grid comes out bit-identical at consteval and at runtime. Changing these constants is a format break.

Cardinality

M(n, d) = Σ_i  M(n/2, snap(d/cos η_i)) · M(n/2, snap(d/sin η_i))

with the 4D base case below. Note that M is genuinely large (log₂ M ≈ 147.7 at dim 64 level 7, i.e. 148 shape bits) therefore it's carried in micron::arbuint<1024>; only M mod 2^64 is kept in the skeleton nodes.

4D base case

S³ foliates into flat tori T_η = S¹_{cos η} × S¹_{sin η}, each carrying n internal circles of m equidistant points, consecutive circles shifted by half a step. (This n is the code's circle count, following the paper and s3.hpp; it is not the block dimension.)

m  = ⌊ π / asin( d / (2 cos η) ) ⌋      — m = 1 when d > 2 cos η
n₂ = ⌊ 2π / asin( d / (2 sin η) ) ⌋     — n = 1 outright when d > 2 sin η
arg = d²/(4 sin²η) − (cos²η / sin²η) · sin²(π / 2m)
n₁ = ⌊ π / asin(√arg) ⌋   when 0 < arg ≤ 1;  n₁ = 1 when √arg > 1;  else n₁ = n₂
n  = max( 2·⌊ min(n₁, n₂) / 2 ⌋ , 1 )   — forced even (so the half-step shift closes)
                                          except the single-circle floor n = 1
M(4, d) = Σ_x m_x · n_x

Note that at d = 2 the whole torus collapses to m = n = 1 and M(4, 2) = 1; exactly what tests/s3.cpp pins at dq_max.

A codeword is

Δξ₁ = 2π/m      ξ₁ = j·Δξ₁ + k·Δξ₁/2
Δξ₂ = 2π/n      ξ₂ = k·Δξ₂
x = ( cos η cos ξ₁,  cos η sin ξ₁,  sin η cos ξ₂,  sin η sin ξ₂ )

Quantizing

Encoding is a descent with no codebook:

η = atan2( √Σ_{c ≥ h} y_c² ,  √Σ_{c < h} y_c² )        h = n/2
i = clamp( round((η − π/4) / Δη), −half, +half )
                                        … then recurse into both halves

and decoding is the same walk in reverse, scaling the two halves by cos η_x and sin η_x.

Index

Per-node fields collapse into one integer by mixed radix, using arbint prefix sums over the leaf fan:

row_off[x] = Σ_{x′ < x} M(c₁(x′)) · M(c₂(x′))
a          = row_off[x] + a₂ · M(c₁) + a₁

Unranking inverts it with a binary search over the monotone prefix sums plus one divmod. Both sides rebuild row_off and M from d_q alone, so the "codebook" is a pure function of the header.

Rate identity

block_elems = n        (the quotient family pins n: quotient 4, quat 8, oct 16)
shape_bits  = ⌈log₂ M⌉ = bit_length(M − 1)
record_bits = (bin|vec ? gain_bits : 0) + shape_bits
stream      = 40 + ⌈ nblocks · record_bits / 8 ⌉ + 8
bits/elem   = record_bits / block_elems

Closed form in (mode, dim, d_q, gain_bits, n_elems) and in nothing about the data — the same for /dev/urandom and for JSON. hsc::rate() returns it without running the codec.

M == 1 gives shape_bits == 0: the shape field has zero width, the record carries the gain alone, and the block reconstructs to one fixed direction times a scalar. That is data loss wearing a compression ratio, and hsc::degenerate() is the query for it. At d ≥ 1 every dim ≥ 8 collapses to it, and dim ≥ 16 is already there at level 1.

Distortion via packing

SCHF guarantees that codewords are ≥ d apart. It says nothing about the distance from an arbitrary input to the nearest codeword, and uneven energy splits clamp to the edge of the leaf fan and stack down the recursion. So there is no derived distortion bound, only a fitted law:

rmse ≈ min( κ(n) · d · σ ,  √2 · σ )        σ = RMS of the centered input
κ(n) = 0.55, 0.81, 1.15, 1.70, 2.35  at n = 4, 8, 16, 32, 64   —  tracking 0.29·√n

√2·σ is the no-information level: error and signal uncorrelated. The fit holds to a pooled median relative error of 0.09 wherever a cell is shippable (PSNR ≥ 20 dB) — per-corpus medians run 0.05–0.14 on the ten non-flat corpora. On flat data it over-predicts badly (median ~0.87 on the runs corpus: a constant run centers onto (1..1)/√n, i.e. η = π/4 exactly — the densest leaf) and it under-predicts as a cell approaches saturation. Those domains are reported separately in BENCHMARKS.md rather than averaged into one flattering number.

Compression by symmetry (quotient)

Read a 4-float block as (z₀, z₁) = (y₀ + i y₁, y₂ + i y₃) ∈ S³. If the data is insensitive to a global phase q ~ q·e^{iψ}, the fiber coordinate is pure redundancy. The Hopf map

h(z₀, z₁) = ( 2 z₀ z̄₁ ,  |z₀|² − |z₁|² )        S³ → S² = S³/S¹

projects onto the quotient, and only the class is stored. h(λq) = λ²·h(q), so dividing by ‖q‖² normalizes with no square root anywhere. S² gets a latitude-band code:

Δθ = 2·asin(d/2)     T = ⌊π/Δθ⌋ + 1     θ₀ = (π − (T−1)Δθ)/2
m_b = ⌊ π / asin( d / (2 sin θ_b) ) ⌋   (m_b = 1 when d > 2 sin θ_b)     M = Σ_b m_b

which is exactly log₂(1/d)-ish bits cheaper than the full S³ code at the same d — measured 2 bits/block at level 3 growing to 4 at level 7. Decoding returns the canonical section z₀ = √((1+p₂)/2) real ≥ 0, z₁ = (p₀ − i p₁)/(2 z₀).

The full Hopf family (quat, oct)

The Hopf family of sphere-by-sphere fibrations has exactly four members; the three with positive-dimensional fiber are the ones a codec can exploit, and hsc implements all of them:

mode::quotient: S³ → S² = S³/S¹ mode::quat: S⁷ → S⁴ = S⁷/S³ mode::oct: S¹⁵ → S⁸ = S¹⁵/S⁷

(The fourth is the real double cover S¹ → S¹ with fiber S⁰ — a zero-dimensional fiber saves no bits.) The Hopf Invariant One Theorem (Adams' theorem: maps of Hopf invariant one exist only for n ∈ {1, 2, 4, 8}) rules out any fibration of a sphere by spheres beyond these four.

All three use the same map shape:

h(x, y) = ( 2 x ȳ ,  |x|² − |y|² )         normalized by ‖(x,y)‖², scale-invariant

The base retains the projective point [x : y]; the fiber is the discarded symmetry. For quaternions, this is exact invariance under simultaneous right multiplication:

(q₀, q₁) → (q₀g, q₁g)

Left multiplication is not quotiented and rotates the base. Octonions have no corresponding group action because multiplication is non-associative; their fibers are the graph spheres { (a, q·a) : |a| = 1 }

where q = y·x⁻¹.

Component convention: Our codec uses vector-first, scalar-last components

quaternion: (x, y, z, w),

octonion: (e₁..e₇, e₀) with e₁,e₂,e₃ = i,j,k, e₄ = ℓ, e₅,e₆,e₇ = iℓ,jℓ,kℓ (Cayley–Dickson construction).

Pairs are adjacent: f[0..n-1], f[n..2n-1]. This matches pose-data memory layouts; quotient's frozen (re, im) scalar-first reading does not change.

The bases S⁴ ⊂ ℝ⁵ and S⁸ ⊂ ℝ⁹ get a cap-anchored, equator-anchored suspension code (sphere/susp.hpp), different from the S² band layout above. The poles are exact codewords ([x:0] and [0:y] classes, spinor basis states) and the interior band count is odd with the equator a band center.

Measured fiber savings vs unit at the same d (level 6): quat 22 → 15 bits/block (7 saved), oct 37 → 24 (13 saved), growing to 10 and 20 at level 7. The quat index stays u64 end to end (M = 2^58.06 at the d = 1e-4 floor, measured); the oct index crosses u64 at level 11 (M ≈ 2^66.4 there; level 10, at 59 bits, is the last that fits) and rides the arbint pack lane. Neither mode can go degenerate: even at d = 2 the two poles remain (M = 2).

Pre-rotation

.transform = true (wire flags bit 2) applies

y = (1/√n) · H · D · x        H = Walsh–Hadamard,  D = pinned ±1 diagonal

H = Hᵀ and H² = nI, so (1/√n)H is orthogonal and involutive. Every basis vector maps to an exact 50/50 half-energy split at every recursion level: η = π/4, the densest leaf, making one-hot rows and axis spikes codable. It costs no rate at all, but smooth data prefers the raw basis, so it is off by default, and it is excluded from the whole quotient family, whose Hopf maps need the algebra structure a generic rotation breaks. The butterfly is adds and subs only (no fma); contractions on one side and not the other would break consteval/runtime bit-identity.

Determinism

Both sides rebuild every skeleton from d_q, never from a raw f64. One ulp of drift in asin would move a ⌊⌋ and change M, so every stream carries skel_guard = M mod 2^64: a drifted decoder fails bad_skeleton instead of quietly emitting wrong floats. tests/comptime.cpp proves consteval streams byte-identical to runtime streams, and tests/exact.cpp pins every SIMD kernel against its scalar twin — that equality is the format contract.

Rate and distortion, cell by cell

The rates are exact and data-independent: hsc::rate() returns it without reading a byte or running the codec, and scripts/compression_ratio_model.py reproduces every cell in independent Python. Distortions (rmse/psnr) below are measured on one named corpus, mixed (unstructured heterogeneous, 512 KiB), in bin mode. Per-corpus grids for all eleven corpora are in BENCHMARKS.md. PSNR is 20·log10(255/rmse) over 8-bit full scale.

d is the nominal minimum codeword distance of the level preset; the stream carries d_q = round(d·2^24). bits/elem is record_bits / n — bits per input byte in bin mode, bits per weight in the f32 lanes — so the two ratio columns are the same number read against an 8-bit and a 32-bit source. All rate columns here are payload only (record bits, no frame): a real stream adds a flat 48 bytes (40-byte header + 8-byte trailer), which is why BENCHMARKS.md's measured 512 KiB ratios read a hair lower (5.56x vs 5.57x at dim64 L6). Both ratio columns are computed from the exact record_bits / n, not from the rounded bits/w shown.

dimLdshape bitsrecord bitsbits/wvs bytesvs fp32rmsepsnr
41.94123.002.67x10.67x38.5516.41
42.76143.502.29x9.14x21.8521.34
43.58164.002.00x8.00x16.2223.93
44.49174.251.88x7.53x13.9025.27
45.310184.501.78x7.11x9.7928.31
46.212205.001.60x6.40x6.1532.35
47.115235.751.39x5.57x2.9938.62
48.0518266.501.23x4.92x1.5444.36
49.0222307.501.07x4.27x0.6651.80
410.0125338.250.97x3.88x0.3656.94
411.00528369.000.89x3.56x0.1465.12
412.002324010.000.80x3.20x0.0098.34
413.001354310.750.74x2.98x0.0099.99
4145e-4384611.500.70x2.78x0.0099.99
81.96141.754.57x18.29x48.5714.40
82.79172.133.76x15.06x38.2816.47
83.512202.503.20x12.80x28.3619.08
84.414222.752.91x11.64x20.8921.73
85.317253.132.56x10.24x16.9523.55
86.222303.752.13x8.53x9.8828.24
87.129374.631.73x6.92x4.7534.59
88.0536445.501.45x5.82x2.3640.67
89.0245536.631.21x4.83x0.9948.19
810.0152607.501.07x4.27x0.5553.32
811.00559678.380.96x3.82x0.2659.67
812.00269779.630.83x3.32x0.0869.64
161.9080.50— †— †102.927.88
162.713211.316.10x24.38x49.1214.31
163.516241.505.33x21.33x42.2715.61
164.421291.814.41x17.66x35.4717.13
165.328362.253.56x14.22x22.4621.10
166.237452.812.84x11.38x15.6924.22
167.152603.752.13x8.53x7.1331.06
168.0568764.751.68x6.74x3.4737.32
169.0288966.001.33x5.33x1.4245.11
1610.011031116.941.15x4.61x0.7650.52
1611.0051181267.881.02x4.06x0.4255.71
321.9080.25— †— †102.997.87
322.714220.6911.64x46.55x102.517.92
323.519270.849.48x37.93x98.498.26
324.429371.166.92x27.68x45.5514.96
325.339471.475.45x21.79x36.2616.94
326.257652.033.94x15.75x23.4220.74
327.190983.062.61x10.45x10.8127.45
328.051221304.061.97x7.88x5.0834.01
329.021641725.381.49x5.95x2.0242.01
3210.011952036.341.26x5.04x1.0547.73
641.9080.13— †— †102.987.88
642.715230.3622.26x89.04x102.987.88
643.521290.4517.66x70.62x102.847.89
644.432400.6312.80x51.20x100.658.07
645.358661.037.76x31.03x47.2914.64
646.284921.445.57x22.26x34.6817.33
647.11481562.443.28x13.13x16.2123.94
648.052142223.472.31x9.23x7.3530.80
649.022993074.801.67x6.67x2.8738.96

Degenerate, not compression. shape_bits == 0 means the recursion collapsed to M == 1: the shape field has zero width, the record is the gain alone, and the direction is gone. The apparent 16x/32x/64x is pure data loss — the measured RMSE sits at the √2·σ no-information level — so no ratio is quoted for those rows. hsc::degenerate() is the query, and the quant.hpp porcelain refuses such a plan unless you type out allow_degenerate. The high-ratio region that is real is the dim axis: dim32 L2 = 11.6x, dim64 L2 = 22.3x, dim64 L3 = 17.7x, at honest (if large) distortion.

Two readings worth taking from the table. Down a column, dimension buys rate: the same d = 0.2 costs 5.00 bits/elem at dim 4 and 1.44 at dim 64. Across a row, d buys accuracy at roughly rmse ∝ d. The two are the whole trade-off surface, and hsc::plan_for() walks it for you.

Bit-exact bytes. The psnr 99.99 in the two finest dim-4 rows is not "very good" — the bench clamps psnr to 99.99 below rmse 1e-9, and an integer rmse is either 0 or ≥ 1/√n, so those cells are exact. mode::bin reconstructs integers, so once the worst per-element error falls under half a count the bytes round back to themselves. hsc::opts_exact_bytes names that cell (dim 4, L13, gain_bits 8) and hsc::exact_bytes(o) is the predicate; at compile time hsc::ct::exact<Src, Back>() proves it for one payload, which is what examples/07_comptime.cpp does. Read the price honestly: 10.75 bits/byte, ratio 0.744x — that is expansion, and it must never be quoted as compression. Note also which axis got you there: level up, dim down. Since kappa(dim) ≈ 0.29·√dim, a wider block is less accurate at the same d, and dim 4 is the only lane where the fine end of the ladder is reachable inside hsc::ct's fixed comptime arenas (dim 8 caps at L11, dim 16 at L10, dim 32/64 at L8). hsc::bin_exact_level(dim_log2) answers 0 for every other dim, meaning unmeasured, not no: the corpus sweep stops at d8 L12, d16 L11, d32 L10, d64 L9.

Preset ladder

quant.hpp provides for seven predefined quantization defaults: one cell per dim (plus q_balanced), each Pareto-optimal within its own dim column. The ladder is not a Pareto frontier over the whole dim × {L5, L6, L7} grid — dim8 L7 (4.625 b/w, est 0.081) strictly dominates q_finest, and dim16 L7 matches q_fine's rate at lower estimated error; reach those cells through hsc::plan_for().

presetdimLdbits/weightvs fp32est rel-rmsemeasured rel-rmsepsnr dB
q_min645.31.0312531.03x0.7050.71029.28
q_tiny646.21.437522.26x0.4700.56131.33
q_small326.22.0312515.75x0.3400.37434.86
q_compact166.22.812511.38x0.2300.24638.49
q_balanced327.13.062510.45x0.1700.17141.64
q_fine86.23.758.53x0.1620.16142.17
q_finest46.25.06.40x0.1100.10046.28

est rel-rmse is the fitted law at that cell, available at compile time from the shape alone. measured is a real round-trip of the first 4096 rows of all-MiniLM-L6-v2's embeddings.word_embeddings.weight (384 wide) through mode::vec; PSNR there is 20·log10(peak/rmse) against the tensor's own peak magnitude, which is not the 8-bit-full-scale PSNR of the table above. Rates shown are the payload rate the constants state; the frame is a flat 48 bytes per stream.

For context, on that same tensor int4 costs 4 bits/weight at rel-RMSE 0.140 and int2 costs 2 at 0.837 (int-N billed at exactly N by the usual convention — the per-row f16 scale, 16/cols, is not counted), while zstd-19 — lossless, so no error at all — manages 1.19x. The full chart, with the lossless coders and int-N in one table, is the opening section of BENCHMARKS.md.

Model and vector quantization

A tensor is not a flat run of elements, so the codec verbs above are the wrong altitude for one. quant.hpp is the porcelain that knows about rows, bits per weight and the lanes that are wrong for a weight matrix:

hsc::tensor t = hsc::tensor::of(weights, 384);            // [n, 384] row-major f32

auto q = hsc::quantize(t, { .bits_per_weight = 3.0 });    // result<qstream>: bytes + the plan
auto e = hsc::measure(t, hsc::floats{...});               // rel-rmse / cosine / psnr

auto blob = hsc::pack(t, { .rel_rmse = 0.2 });            // self-describing HSCQ container
hsc::qreader rd{ blob_view, sc };
rd.row(9182, out);                                        // one vector, O(cols), no table decode

Build/test with duck: duck batch test.duck · benches duck batch build.duck · examples duck batch examples.duck · comptime stress scripts/ctbuild. Never -Ofast (floating-point codec path). MIT license.

Contributors

rfgplk

3 commits

Languages

C++

82.5%

Python

12.6%

Shell

4.9%