Many measurement problems end with the same question: a device reports a point tt that should lie on a known lattice, but noise has pushed it off. Lattice codes in digital communication, integer ambiguity resolution in satellite positioning and rounding in signal processing all ask for the lattice point vv that produced tt. That is the closest vector problem. This guide shows the most common way to hand it to a lattice reducer: the Kannan embedding, which turns “close to the lattice” into “short in a lattice one dimension larger”. Rows of every matrix are basis vectors, as in g6k-rs.

CVP and bounded distance decoding

Given a basis BB of a lattice LL and a target tt, CVP asks for a lattice point nearest to tt. In a decoding problem we know more: tt was produced as

t=v+e,v∈L,e small.t = v + e, \qquad v \in L, \quad e \text{ small}.

When ∥e∥<λ1(L)/2\|e\| < \lambda_1(L)/2, the point vv is the unique closest lattice point, and the problem is called bounded distance decoding (BDD). Past that radius another lattice point can be as close as vv, so success means recovering the planted vv, not merely some nearby point. We measure noise relative to the Gaussian heuristic gh(L)≈n/(2πe) det⁡(L)1/n\mathrm{gh}(L) \approx \sqrt{n/(2\pi e)}\,\det(L)^{1/n}, which estimates λ1\lambda_1 for a random-looking lattice.

The embedding

The small object is the error ee. The relation t−e∈Lt - e \in L is affine, because of the constant tt, so we homogenize it: append one coordinate to every basis vector and add tt as a new row with an embedding factor MM in the new coordinate.

B′=(B0tM)∈Z(n+1)×(n+1)B' = \begin{pmatrix} B & 0 \\ t & M \end{pmatrix} \in \mathbb{Z}^{(n+1) \times (n+1)}

Every vector of the new lattice L′L' has the form (xB+kt,  kM)(xB + k t,\; kM) for integers xx and kk. Take k=−1k = -1 and choose xx with xB=vxB = v. The result is

(v−t,  −M)=(−e,  −M),∥(e,M)∥=∥e∥2+M2.(v - t,\; -M) = (-e,\; -M), \qquad \|(e, M)\| = \sqrt{\|e\|^2 + M^2}.

Kannan embedding at a glance

  • Dimension: n+1n + 1.
  • Determinant: det⁡(L′)=M⋅det⁡(L)\det(L') = M \cdot \det(L), because B′B' is block triangular.
  • Target: ±(e,M)\pm(e, M) with norm ∥e∥2+M2\sqrt{\|e\|^2 + M^2}.
  • Heuristic condition: ∥e∥2+M2<gh(L′)=n+12πe (Mdet⁡L)1/(n+1)\sqrt{\|e\|^2+M^2} < \mathrm{gh}(L') = \sqrt{\tfrac{n+1}{2\pi e}}\,(M\det L)^{1/(n+1)}.
  • Extraction: find a reduced row whose last coordinate is ±M\pm M, flip its sign so the last entry is +M+M, read off ee, and return v=t−ev = t - e.

The heuristic condition says the target is shorter than a typical shortest vector of a random lattice with the same volume. The planting ratio gh(L′)/∥(e,M)∥\mathrm{gh}(L')/\|(e,M)\| measures the margin: larger is better. It is a heuristic for random-looking lattices, not a proof that (e,M)(e, M) is the shortest vector.

Choosing the embedding factor M

MM is a weight on one extra coordinate. Reduction minimizes the Euclidean norm, so a balanced target has every coordinate of roughly the same size. The coordinates of ee have typical size σ\sigma, the per-coordinate noise level, so the balanced choice is M≈σM \approx \sigma, not M≈∥e∥≈σnM \approx \|e\| \approx \sigma\sqrt{n}. The difference matters. With M=∥e∥M = \|e\| the target norm grows by a factor 2\sqrt 2, while gh(L′)\mathrm{gh}(L') grows only like M1/(n+1)M^{1/(n+1)}.

The table uses the lattice from the experiments below (n=40n = 40, det⁡(L)=1000720\det(L) = 10007^{20}, so gh(L)≈153.1\mathrm{gh}(L) \approx 153.1).

∥e∥/gh(L)\|e\|/\mathrm{gh}(L)Mtarget normgh(L′)\mathrm{gh}(L')planting ratio
0.5σ≈12.1\sigma \approx 12.177.5147.21.90
0.5∥e∥≈76.5\|e\| \approx 76.5108.3154.01.42
0.7σ≈16.9\sigma \approx 16.9108.5148.41.37
0.7∥e∥≈107.2\|e\| \approx 107.2151.6155.31.02
0.9σ≈21.8\sigma \approx 21.8139.5149.31.07
0.9∥e∥≈137.8\|e\| \approx 137.8194.9156.20.80

Both extremes fail in instructive ways:

Measured on a fixed set of 40 random instances (n=40n = 40, separate from the instances in the larger table below), counting how often the planted vv was recovered:

M∥e∥/gh=0.7\|e\|/\mathrm{gh} = 0.7, LLL∥e∥/gh=0.9\|e\|/\mathrm{gh} = 0.9, BKZ-20
131/4023/40
0.25 σ0.25\,\sigma31/4030/40
σ\sigma27/4027/40
2σ2\sigma19/4024/40
4σ4\sigma13/406/40
n σ≈∥e∥\sqrt{n}\,\sigma \approx \|e\|13/406/40
8n σ8\sqrt{n}\,\sigma13/406/40
Babai, same reduction13/404/40

Anything from M=1M = 1 to about 2σ2\sigma works well; from 4σ4\sigma upward the embedding does no better than Babai. A rule that holds up: start at M=⌈σ⌉M = \lceil \sigma \rceil and try a smaller value if you see ±2M\pm 2M in the last coordinate of short rows.

What can beat the target

The planting ratio compares the target with a random lattice. Before trusting it, check the vectors this particular construction is guaranteed to contain:

A complete example in Rust

The program below builds a random 30-dimensional lattice with det⁡(L)=1000715\det(L) = 10007^{15}, hides a point vv, adds noise with σ=17\sigma = 17 (so ∥e∥≈0.7 gh(L)\|e\| \approx 0.7\,\mathrm{gh}(L)), and decodes it both ways after LLL and BKZ-10. It needs only g6k-rs = "=0.1.0" in Cargo.toml.

// Decode a noisy lattice point two ways: Babai nearest plane and Kannan embedding.
// Rows of every matrix are basis vectors. Cargo.toml: g6k-rs = "=0.1.0"
use g6k_rs::{BKZParams, GSO, LatticeBasis, babai_nearest_plane, bkz_reduce, lll};

const N: usize = 30; // lattice dimension
const Q: i64 = 10007; // det(L) = Q^(N/2)

// Small deterministic generator so the example needs no extra crates.
struct Rng(u64);
impl Rng {
    fn next(&mut self) -> u64 {
        self.0 = self.0.wrapping_add(0x9E3779B97F4A7C15);
        let mut z = self.0;
        z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
        z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
        z ^ (z >> 31)
    }
    fn below(&mut self, m: u64) -> i64 { (self.next() % m) as i64 }
    fn gauss(&mut self) -> f64 {
        let u = ((self.next() >> 11) as f64 / (1u64 << 53) as f64).max(1e-300);
        let v = (self.next() >> 11) as f64 / (1u64 << 53) as f64;
        (-2.0 * u.ln()).sqrt() * (2.0 * std::f64::consts::PI * v).cos()
    }
}

fn reduce(b: &mut LatticeBasis) {
    let _ = lll(b);
    let _ = bkz_reduce(b, &BKZParams { block_size: 10, max_tours: 8, ..Default::default() });
}

fn main() {
    let mut rng = Rng(2);

    // 1. A known lattice: rows [Q*I | 0] and [A | I].
    let k = N / 2;
    let mut b = vec![vec![0i64; N]; N];
    for i in 0..k { b[i][i] = Q; }
    for i in k..N {
        for j in 0..k { b[i][j] = rng.below(Q as u64); }
        b[i][i] = 1;
    }

    // 2. A hidden lattice point v, received as t = v + e.
    let x: Vec<i64> = (0..N).map(|_| rng.below(21) - 10).collect();
    let v: Vec<i64> = (0..N).map(|j| (0..N).map(|i| x[i] * b[i][j]).sum()).collect();
    let sigma = 17.0; // per-coordinate noise size
    let e: Vec<i64> = (0..N).map(|_| (sigma * rng.gauss()).round() as i64).collect();
    let t: Vec<i64> = (0..N).map(|j| v[j] + e[j]).collect();
    let e_norm = e.iter().map(|&c| (c * c) as f64).sum::<f64>().sqrt();
    println!("||e|| = {:.1}", e_norm);

    // 3a. Babai nearest plane on a reduced basis.
    let mut lb = LatticeBasis::from_integers(&b);
    reduce(&mut lb);
    let gso = GSO::new(&lb);
    let tf: Vec<f64> = t.iter().map(|&c| c as f64).collect();
    let np = babai_nearest_plane(&lb, &gso, &tf);
    let babai_ok = np.closest.iter().zip(&v).all(|(a, &b)| (a - b as f64).abs() < 0.5);
    println!("Babai nearest plane: distance {:.1}, found v: {}", np.dist_sq.sqrt(), babai_ok);

    // 3b. Kannan embedding: rows [B | 0] and [t | M], with M about sigma.
    let m = sigma.round() as i64;
    let mut rows: Vec<Vec<i64>> = b.iter().map(|r| { let mut r = r.clone(); r.push(0); r }).collect();
    let mut last = t.clone();
    last.push(m);
    rows.push(last);
    let mut kb = LatticeBasis::from_integers(&rows);
    reduce(&mut kb);

    // 4. Extract: a row with last coordinate +-M is +-(e, M).
    for (i, r) in kb.basis.iter().enumerate() {
        if (r[N].abs() - m as f64).abs() < 0.5 {
            let s = r[N].signum();
            let e_hat: Vec<i64> = (0..N).map(|j| (s * r[j]).round() as i64).collect();
            let v_hat: Vec<i64> = (0..N).map(|j| t[j] - e_hat[j]).collect();
            println!("Kannan: row {} has last coordinate {}, norm {:.1}, found v: {}",
                i, r[N], kb.norm_sq(i).sqrt(), v_hat == v);
            break;
        }
    }
}

Output with seed 2 (cargo run --release):

||e|| = 100.4
Babai nearest plane: distance 171.7, found v: false
Kannan: row 0 has last coordinate 17, norm 101.8, found v: true

This seed was chosen to show a case where the two methods disagree; with most seeds at this noise level both succeed. Babai returned a lattice point at distance 171.7, farther than the true error of 100.4, because ee did not fit inside the Gram–Schmidt box of the reduced basis. The embedding found (e,17)(e, 17) as the first reduced row, with norm 100.42+172≈101.8\sqrt{100.4^2 + 17^2} \approx 101.8.

Measured success rates at n = 20, 30, 40

Each cell counts recoveries of the planted point over 40 random instances (fixed seeds, identical instances in every column). Kannan uses MM equal to σ\sigma rounded to the nearest integer. Babai runs nearest plane on the basis reduced the same way.

n∥e∥/gh\|e\|/\mathrm{gh}LLL: Babai / KannanBKZ-10: Babai / KannanBKZ-20: Babai / Kannan
200.540 / 4039 / 4040 / 40
200.736 / 4033 / 4036 / 40
200.921 / 3025 / 3119 / 30
201.18 / 159 / 187 / 17
300.538 / 4038 / 4039 / 40
300.725 / 3733 / 4025 / 40
300.914 / 1914 / 3410 / 36
301.12 / 33 / 124 / 10
400.536 / 4039 / 4040 / 40
400.716 / 2525 / 4014 / 39
400.94 / 74 / 193 / 30
401.10 / 20 / 60 / 8

Three patterns stand out.

With 40 trials a count moves by a few points from sampling alone, which explains small inversions such as Babai doing slightly better after BKZ-10 than after BKZ-20 in some rows. On the n=40n = 40 lattice the reductions themselves are ordered as expected: the first basis vector has norm 220.6 after LLL, 178.4 after BKZ-10, 174.3 after BKZ-20 and 161.8 after BKZ-30.

Scaling up

The embedding adds a single dimension, so the cost is that of reducing an (n+1)(n+1)-dimensional lattice well enough to expose a vector that is shorter than expected by the planting ratio. Smaller ratios need larger BKZ block sizes, which is where sieve-backed BKZ and progressive strategies from the recipes page come in; the BKZ chapter explains the trade-off. The same homogenization step reappears whenever an equation has a constant term, most directly in short solutions of Ax = b.

Common mistakes

Further reading

Ravi Kannan, “Minkowski’s convex body theorem and integer programming”, Mathematics of Operations Research 12(3), 1987, introduces the embedding. László Babai, “On Lovász’ lattice reduction and the nearest lattice point problem”, Combinatorica 6, 1986, gives the nearest-plane algorithm. For the positioning application, see P. J. G. Teunissen, “The least-squares ambiguity decorrelation adjustment”, Journal of Geodesy 70, 1995. Daniele Micciancio and Shafi Goldwasser’s book Complexity of Lattice Problems (2002) covers CVP, BDD and the reductions between them.