Skip to main content

cram_codec/
tensorize.rs

1//! Quantized tensorization: an `n₁×…×n_D` grid → a `d`-way tensor with tiny
2//! mode sizes (mostly 2).
3//!
4//! Each axis of length `p` is written in mixed radix, `p = f₁·f₂·…·f_L`, and
5//! every digit becomes one tensor mode. With `p = 2^L` this is the classic
6//! binary QTT ("quantics") tensorization. The digit *order* across axes is a
7//! choice:
8//!
9//! - [`DigitOrder::RowMajor`] — all digits of axis 0 (most significant
10//!   first), then axis 1, …: the tensor index is the row-major linear index.
11//! - [`DigitOrder::Morton`] — digits interleaved coarse-to-fine, aligned at
12//!   the least significant digit (Z-order for powers of two). Neighbouring
13//!   cells share long index prefixes along *every* axis.
14//!
15//! Lengths that are not `7`-smooth are padded. [`PadPolicy::MixedRadix`]
16//! pads only to the next 7-smooth length (factors 2,3,5,7 become modes of that
17//! size; odd factors are placed most significant), [`PadPolicy::Pow2`] pads to
18//! the next power of two (pure binary QTT). Padding is mirror reflection, so
19//! the padded field stays continuous at the seam; the padded cells are coded
20//! (they cost bits) but are cropped on decode and never enter error metrics.
21
22/// How digits of different axes are ordered in the tensor.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum DigitOrder {
25    /// Row-major bit order (axis 0's digits first).
26    RowMajor,
27    /// Bit-interleaved (Z-order / Morton).
28    Morton,
29}
30
31/// How non-power-of-two lengths are handled.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum PadPolicy {
34    /// Factor into 2,3,5,7 modes; pad only to the next 7-smooth length.
35    MixedRadix,
36    /// Pad every axis to the next power of two (binary modes only).
37    Pow2,
38}
39
40/// The tensorization of one grid.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct Layout {
43    /// Original grid shape (row-major, last axis fastest).
44    pub dims: Vec<usize>,
45    /// Padded grid shape.
46    pub padded: Vec<usize>,
47    /// Tensor mode sizes, most significant first.
48    pub modes: Vec<usize>,
49    /// For each tensor mode, the stride of that digit in the padded grid.
50    pub strides: Vec<usize>,
51    /// Digit order used.
52    pub order: DigitOrder,
53    /// Padding policy used.
54    pub pad: PadPolicy,
55}
56
57fn smooth_factors(mut p: usize) -> Option<Vec<usize>> {
58    let mut f = Vec::new();
59    for q in [7usize, 5, 3, 2] {
60        while p.is_multiple_of(q) {
61            f.push(q);
62            p /= q;
63        }
64    }
65    (p == 1).then_some(f)
66}
67
68/// Axis length after padding under `pad`, and its digits (most significant first).
69pub fn padded_len(n: usize, pad: PadPolicy) -> (usize, Vec<usize>) {
70    assert!(n >= 1);
71    match pad {
72        PadPolicy::Pow2 => {
73            let p = n.next_power_of_two();
74            (p, vec![2; p.trailing_zeros() as usize])
75        }
76        PadPolicy::MixedRadix => {
77            let mut p = n;
78            loop {
79                if let Some(f) = smooth_factors(p) {
80                    // smooth_factors yields 7s,5s,3s then 2s: odd radices most
81                    // significant, binary digits finest.
82                    return (p, f);
83                }
84                p += 1;
85            }
86        }
87    }
88}
89
90impl Layout {
91    /// Build the tensorization of a grid of shape `dims`.
92    pub fn new(dims: &[usize], order: DigitOrder, pad: PadPolicy) -> Self {
93        assert!(!dims.is_empty(), "Layout: empty shape");
94        let mut padded = Vec::new();
95        let mut digits: Vec<Vec<usize>> = Vec::new();
96        for &n in dims {
97            let (p, f) = padded_len(n, pad);
98            padded.push(p);
99            digits.push(f);
100        }
101        // Row-major strides of the padded grid.
102        let mut axis_stride = vec![1usize; dims.len()];
103        for a in (0..dims.len().saturating_sub(1)).rev() {
104            axis_stride[a] = axis_stride[a + 1] * padded[a + 1];
105        }
106        // Weight of each digit within its axis (product of finer digits).
107        let digit_weight = |a: usize, j: usize| -> usize { digits[a][j + 1..].iter().product() };
108        let mut modes = Vec::new();
109        let mut strides = Vec::new();
110        match order {
111            DigitOrder::RowMajor => {
112                for a in 0..dims.len() {
113                    for j in 0..digits[a].len() {
114                        modes.push(digits[a][j]);
115                        strides.push(digit_weight(a, j) * axis_stride[a]);
116                    }
117                }
118            }
119            DigitOrder::Morton => {
120                let lmax = digits.iter().map(|d| d.len()).max().unwrap_or(0);
121                for level in (1..=lmax).rev() {
122                    for a in 0..dims.len() {
123                        let l = digits[a].len();
124                        if l >= level {
125                            let j = l - level;
126                            modes.push(digits[a][j]);
127                            strides.push(digit_weight(a, j) * axis_stride[a]);
128                        }
129                    }
130                }
131            }
132        }
133        if modes.is_empty() {
134            // A single cell: one trivial mode keeps the TT machinery uniform.
135            modes.push(1);
136            strides.push(1);
137        }
138        Self {
139            dims: dims.to_vec(),
140            padded,
141            modes,
142            strides,
143            order,
144            pad,
145        }
146    }
147
148    /// Number of original cells.
149    pub fn len(&self) -> usize {
150        self.dims.iter().product()
151    }
152
153    /// Whether the grid is empty (never, by construction).
154    pub fn is_empty(&self) -> bool {
155        self.len() == 0
156    }
157
158    /// Number of tensor entries (padded cells).
159    pub fn tensor_len(&self) -> usize {
160        self.modes.iter().product()
161    }
162
163    /// Mirror-pad `data` (shape `dims`) to the padded grid.
164    pub fn pad_grid(&self, data: &[f64]) -> Vec<f64> {
165        assert_eq!(data.len(), self.len());
166        if self.dims == self.padded {
167            return data.to_vec();
168        }
169        let nd = self.dims.len();
170        let total: usize = self.padded.iter().product();
171        let mut src_stride = vec![1usize; nd];
172        for a in (0..nd.saturating_sub(1)).rev() {
173            src_stride[a] = src_stride[a + 1] * self.dims[a + 1];
174        }
175        let mut out = Vec::with_capacity(total);
176        let mut idx = vec![0usize; nd];
177        for _ in 0..total {
178            let mut off = 0;
179            for a in 0..nd {
180                off += reflect(idx[a], self.dims[a]) * src_stride[a];
181            }
182            out.push(data[off]);
183            for a in (0..nd).rev() {
184                idx[a] += 1;
185                if idx[a] < self.padded[a] {
186                    break;
187                }
188                idx[a] = 0;
189            }
190        }
191        out
192    }
193
194    /// Padded grid → tensor order (gather).
195    pub fn to_tensor(&self, padded: &[f64]) -> Vec<f64> {
196        let n = self.tensor_len();
197        assert_eq!(padded.len(), n);
198        if self.is_identity() {
199            return padded.to_vec();
200        }
201        let mut out = Vec::with_capacity(n);
202        self.odometer(|src| out.push(padded[src]));
203        out
204    }
205
206    /// Tensor order → original grid (scatter + crop).
207    pub fn from_tensor(&self, tensor: &[f64]) -> Vec<f64> {
208        let n = self.tensor_len();
209        assert_eq!(tensor.len(), n);
210        let grid = if self.is_identity() {
211            tensor.to_vec()
212        } else {
213            let mut g = vec![0.0; n];
214            let mut t = 0;
215            self.odometer(|src| {
216                g[src] = tensor[t];
217                t += 1;
218            });
219            g
220        };
221        self.crop(&grid)
222    }
223
224    fn crop(&self, grid: &[f64]) -> Vec<f64> {
225        if self.dims == self.padded {
226            return grid.to_vec();
227        }
228        let nd = self.dims.len();
229        let mut pstride = vec![1usize; nd];
230        for a in (0..nd.saturating_sub(1)).rev() {
231            pstride[a] = pstride[a + 1] * self.padded[a + 1];
232        }
233        let mut out = Vec::with_capacity(self.len());
234        let mut idx = vec![0usize; nd];
235        for _ in 0..self.len() {
236            let off: usize = (0..nd).map(|a| idx[a] * pstride[a]).sum();
237            out.push(grid[off]);
238            for a in (0..nd).rev() {
239                idx[a] += 1;
240                if idx[a] < self.dims[a] {
241                    break;
242                }
243                idx[a] = 0;
244            }
245        }
246        out
247    }
248
249    fn is_identity(&self) -> bool {
250        // Row-major digit order with strides equal to the natural ones.
251        let mut s = 1usize;
252        for k in (0..self.modes.len()).rev() {
253            if self.strides[k] != s {
254                return false;
255            }
256            s *= self.modes[k];
257        }
258        true
259    }
260
261    /// Visit the padded-grid offset of every tensor entry, in tensor order.
262    fn odometer(&self, mut f: impl FnMut(usize)) {
263        let d = self.modes.len();
264        let n = self.tensor_len();
265        let mut digit = vec![0usize; d];
266        let mut off = 0usize;
267        for _ in 0..n {
268            f(off);
269            let mut k = d;
270            while k > 0 {
271                k -= 1;
272                digit[k] += 1;
273                off += self.strides[k];
274                if digit[k] < self.modes[k] {
275                    break;
276                }
277                off -= self.strides[k] * self.modes[k];
278                digit[k] = 0;
279            }
280        }
281    }
282}
283
284/// Mirror index `i` into `0..n` (reflection without repeating the edge).
285fn reflect(i: usize, n: usize) -> usize {
286    if n == 1 {
287        return 0;
288    }
289    let period = 2 * (n - 1);
290    let r = i % period;
291    if r < n {
292        r
293    } else {
294        period - r
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn rowmajor_pow2_is_identity() {
304        let l = Layout::new(&[8, 4], DigitOrder::RowMajor, PadPolicy::Pow2);
305        assert_eq!(l.modes, vec![2; 5]);
306        let x: Vec<f64> = (0..32).map(|i| i as f64).collect();
307        assert_eq!(l.to_tensor(&l.pad_grid(&x)), x);
308    }
309
310    #[test]
311    fn morton_matches_bit_interleave() {
312        let l = Layout::new(&[4, 4], DigitOrder::Morton, PadPolicy::Pow2);
313        let x: Vec<f64> = (0..16).map(|i| i as f64).collect();
314        let t = l.to_tensor(&x);
315        // Tensor index bits (MSB first) = y1 x1 y0 x0.
316        for (ti, &v) in t.iter().enumerate() {
317            let y = ((ti >> 3) & 1) << 1 | ((ti >> 1) & 1);
318            let xx = ((ti >> 2) & 1) << 1 | (ti & 1);
319            assert_eq!(v, (y * 4 + xx) as f64);
320        }
321    }
322
323    #[test]
324    fn roundtrip_all_policies() {
325        for dims in [
326            vec![1000],
327            vec![12, 20],
328            vec![5, 6, 7],
329            vec![11, 3, 9],
330            vec![1],
331        ] {
332            for order in [DigitOrder::RowMajor, DigitOrder::Morton] {
333                for pad in [PadPolicy::MixedRadix, PadPolicy::Pow2] {
334                    let l = Layout::new(&dims, order, pad);
335                    let n = l.len();
336                    let x: Vec<f64> = (0..n).map(|i| (i as f64).sin()).collect();
337                    let t = l.to_tensor(&l.pad_grid(&x));
338                    assert_eq!(t.len(), l.tensor_len());
339                    assert_eq!(l.from_tensor(&t), x, "{dims:?} {order:?} {pad:?}");
340                }
341            }
342        }
343    }
344
345    #[test]
346    fn mixed_radix_lengths() {
347        assert_eq!(padded_len(384, PadPolicy::MixedRadix).0, 384);
348        assert_eq!(padded_len(384, PadPolicy::Pow2).0, 512);
349        assert_eq!(
350            padded_len(500, PadPolicy::MixedRadix).1,
351            vec![5, 5, 5, 2, 2]
352        );
353        assert_eq!(padded_len(11, PadPolicy::MixedRadix).0, 12);
354        assert_eq!(reflect(5, 4), 1);
355    }
356}