use super::enumerate::Enumerator;
use super::error::LatticeGeometryError;
use super::lll::lattice_reduce_rows;
use super::lll::LatticeError;
use super::quadform;
use rug::{Integer, Rational};
use std::fmt;
use std::sync::OnceLock;
pub const MAX_ENUM_RANK: usize = 24;
pub const DEFAULT_ENUM_NODE_BUDGET: u64 = 50_000_000;
pub const MAX_THETA_NORM: u64 = 1_000_000;
type ReducedEnumerator = (Enumerator, Vec<Vec<Integer>>, Vec<Vec<Rational>>);
type MinimalPass = (Rational, u64, Vec<Vec<Integer>>, Vec<Vec<Integer>>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LatticeVector {
pub coefficients: Vec<Integer>,
pub coordinates: Option<Vec<Rational>>,
pub norm: Rational,
}
pub struct Lattice {
basis: Option<Vec<Vec<Rational>>>,
gram: Vec<Vec<Rational>>,
minimal: OnceLock<MinimalPass>,
}
impl Clone for Lattice {
fn clone(&self) -> Self {
Self {
basis: self.basis.clone(),
gram: self.gram.clone(),
minimal: OnceLock::new(),
}
}
}
impl PartialEq for Lattice {
fn eq(&self, other: &Self) -> bool {
self.basis == other.basis && self.gram == other.gram
}
}
impl Eq for Lattice {}
impl fmt::Debug for Lattice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Lattice")
.field("rank", &self.rank())
.field("ambient_dimension", &self.ambient_dimension())
.field("gram", &self.gram)
.field("basis", &self.basis)
.finish()
}
}
impl Lattice {
pub fn from_basis(rows: &[Vec<Integer>]) -> Result<Self, LatticeGeometryError> {
let rational: Vec<Vec<Rational>> = rows
.iter()
.map(|r| r.iter().map(Rational::from).collect())
.collect();
Self::from_rational_basis(&rational)
}
pub fn from_rational_basis(rows: &[Vec<Rational>]) -> Result<Self, LatticeGeometryError> {
if rows.is_empty() {
return Err(LatticeError::EmptyBasis.into());
}
let cols = rows[0].len();
if cols == 0 {
return Err(LatticeError::EmptyBasis.into());
}
for (i, r) in rows.iter().enumerate() {
if r.len() != cols {
return Err(LatticeError::RaggedBasis {
row: i,
expected_cols: cols,
got_cols: r.len(),
}
.into());
}
}
let gram = quadform::gram_of_rows(rows);
quadform::gram_schmidt(&gram)?;
Ok(Self {
basis: Some(rows.to_vec()),
gram,
minimal: OnceLock::new(),
})
}
pub fn from_gram(g: &[Vec<Rational>]) -> Result<Self, LatticeGeometryError> {
quadform::validate_gram(g)?;
quadform::gram_schmidt(g)?;
Ok(Self {
basis: None,
gram: g.to_vec(),
minimal: OnceLock::new(),
})
}
pub fn rank(&self) -> usize {
self.gram.len()
}
pub fn ambient_dimension(&self) -> usize {
match &self.basis {
Some(b) => b[0].len(),
None => self.gram.len(),
}
}
pub fn gram_matrix(&self) -> &[Vec<Rational>] {
&self.gram
}
pub fn basis(&self) -> Option<&[Vec<Rational>]> {
self.basis.as_deref()
}
pub fn determinant(&self) -> Rational {
quadform::determinant(&self.gram).expect("Gram matrix validated at construction")
}
pub fn covolume(&self) -> f64 {
self.determinant().to_f64().sqrt()
}
pub fn is_integral(&self) -> bool {
self.gram
.iter()
.all(|r| r.iter().all(|x| *x.denom() == 1i32))
}
pub fn is_even(&self) -> bool {
self.is_integral()
&& (0..self.rank()).all(|i| {
let n = self.gram[i][i].numer().clone();
n.is_divisible_u(2)
})
}
pub fn dual(&self) -> Result<Self, LatticeGeometryError> {
let inv = quadform::inverse(&self.gram)?;
let basis = self.basis.as_ref().map(|b| {
let m = b.len();
let n = b[0].len();
(0..m)
.map(|i| {
(0..n)
.map(|c| {
let mut acc = Rational::new();
for t in 0..m {
acc += Rational::from(&inv[i][t] * &b[t][c]);
}
acc
})
.collect()
})
.collect()
});
Ok(Self {
basis,
gram: inv,
minimal: OnceLock::new(),
})
}
fn reduced_enumerator(&self, budget: u64) -> Result<ReducedEnumerator, LatticeGeometryError> {
if self.rank() > MAX_ENUM_RANK {
return Err(LatticeGeometryError::RankTooLarge {
rank: self.rank(),
max: MAX_ENUM_RANK,
});
}
let (g, u) = quadform::gram_lll(&self.gram, &Rational::from((99, 100)))?;
let (mu, b) = quadform::gram_schmidt(&g)?;
Ok((Enumerator::new(mu, b, budget), u, g))
}
fn lift(&self, x: &[Integer], u: &[Vec<Integer>]) -> Vec<Integer> {
let m = u.len();
(0..m)
.map(|t| {
let mut acc = Integer::new();
for (i, xi) in x.iter().enumerate() {
acc += Integer::from(xi * &u[i][t]);
}
acc
})
.collect()
}
fn coordinates_of(&self, coeffs: &[Integer]) -> Option<Vec<Rational>> {
let b = self.basis.as_ref()?;
let n = b[0].len();
Some(
(0..n)
.map(|c| {
let mut acc = Rational::new();
for (t, co) in coeffs.iter().enumerate() {
acc += Rational::from(&b[t][c] * &Rational::from(co));
}
acc
})
.collect(),
)
}
fn vector_from(&self, x: &[Integer], u: &[Vec<Integer>], norm: Rational) -> LatticeVector {
let coefficients = self.lift(x, u);
let coordinates = self.coordinates_of(&coefficients);
LatticeVector {
coefficients,
coordinates,
norm,
}
}
fn minimal_pass(
&self,
budget: u64,
collect: bool,
) -> Result<MinimalPass, LatticeGeometryError> {
let (mut e, u, g) = self.reduced_enumerator(budget)?;
let bound = (0..g.len())
.map(|i| g[i][i].clone())
.fold(None::<Rational>, |acc, x| {
Some(match acc {
Some(a) if a < x => a,
_ => x,
})
});
let bound = bound.ok_or(LatticeError::EmptyBasis)?;
let mut best: Option<Rational> = None;
let mut count: u64 = 0;
let mut reps: Vec<Vec<Integer>> = Vec::new();
let mut witness: Vec<Integer> = Vec::new();
e.enumerate(&bound, None, true, &mut |found| {
if found.norm == 0 {
return;
}
match &best {
Some(b) if found.norm > *b => (),
Some(b) if found.norm == *b => {
count += 1;
if collect {
reps.push(found.x.to_vec());
}
}
_ => {
best = Some(found.norm.clone());
count = 1;
witness = found.x.to_vec();
if collect {
reps.clear();
reps.push(found.x.to_vec());
}
}
}
})?;
let best = best.ok_or(LatticeGeometryError::NotPositiveDefinite { pivot: 1 })?;
if !collect {
reps.push(witness);
}
Ok((best, count * 2, reps, u))
}
fn minimal_cached(&self) -> Result<&MinimalPass, LatticeGeometryError> {
if let Some(v) = self.minimal.get() {
return Ok(v);
}
let computed = self.minimal_pass(DEFAULT_ENUM_NODE_BUDGET, false)?;
let _ = self.minimal.set(computed);
self.minimal.get().ok_or(LatticeGeometryError::Internal {
detail: "the shortest-vector memo was empty immediately after being set",
})
}
pub fn minimum(&self) -> Result<Rational, LatticeGeometryError> {
Ok(self.minimal_cached()?.0.clone())
}
pub fn minimum_with_budget(&self, budget: u64) -> Result<Rational, LatticeGeometryError> {
Ok(self.minimal_pass(budget, false)?.0)
}
pub fn shortest_vector(&self) -> Result<LatticeVector, LatticeGeometryError> {
let (norm, _, reps, u) = self.minimal_cached()?;
let x = reps.first().ok_or(LatticeError::EmptyBasis)?;
Ok(self.vector_from(x, u, norm.clone()))
}
pub fn shortest_vector_with_budget(
&self,
budget: u64,
) -> Result<LatticeVector, LatticeGeometryError> {
let (norm, _, reps, u) = self.minimal_pass(budget, false)?;
let x = reps.first().ok_or(LatticeError::EmptyBasis)?;
Ok(self.vector_from(x, &u, norm))
}
pub fn minimal_vectors(&self) -> Result<Vec<LatticeVector>, LatticeGeometryError> {
self.minimal_vectors_with_budget(DEFAULT_ENUM_NODE_BUDGET)
}
pub fn minimal_vectors_with_budget(
&self,
budget: u64,
) -> Result<Vec<LatticeVector>, LatticeGeometryError> {
let (norm, _, reps, u) = self.minimal_pass(budget, true)?;
let mut out = Vec::with_capacity(reps.len() * 2);
for x in reps {
let v = self.vector_from(&x, &u, norm.clone());
let neg = LatticeVector {
coefficients: v.coefficients.iter().map(|c| Integer::from(-c)).collect(),
coordinates: v
.coordinates
.as_ref()
.map(|c| c.iter().map(|x| -x.clone()).collect()),
norm: norm.clone(),
};
out.push(v);
out.push(neg);
}
Ok(out)
}
pub fn kissing_number(&self) -> Result<u64, LatticeGeometryError> {
Ok(self.minimal_cached()?.1)
}
pub fn kissing_number_with_budget(&self, budget: u64) -> Result<u64, LatticeGeometryError> {
Ok(self.minimal_pass(budget, false)?.1)
}
pub fn theta_series(&self, max_norm: u64) -> Result<Vec<Integer>, LatticeGeometryError> {
self.theta_series_with_budget(max_norm, DEFAULT_ENUM_NODE_BUDGET)
}
pub fn theta_series_with_budget(
&self,
max_norm: u64,
budget: u64,
) -> Result<Vec<Integer>, LatticeGeometryError> {
if max_norm > MAX_THETA_NORM {
return Err(LatticeGeometryError::InvalidParameter {
detail: "theta series bound exceeds MAX_THETA_NORM",
});
}
for (i, row) in self.gram.iter().enumerate() {
for (j, x) in row.iter().enumerate() {
if *x.denom() != 1i32 {
return Err(LatticeGeometryError::NonIntegralGram { row: i, col: j });
}
}
}
let (mut e, _, _) = self.reduced_enumerator(budget)?;
let mut counts = vec![Integer::new(); (max_norm as usize) + 1];
let mut broken: Option<&'static str> = None;
e.enumerate(&Rational::from(max_norm), None, true, &mut |found| {
let slot = if *found.norm.denom() != 1i32 {
None
} else {
found
.norm
.numer()
.to_u64()
.and_then(|n| usize::try_from(n).ok())
.filter(|n| *n < counts.len())
};
match slot {
Some(n) => counts[n] += 1u32,
None => {
broken.get_or_insert(
"an enumerated norm was not an integer in 0..=max_norm, \
despite an integral Gram matrix",
);
}
}
})?;
if let Some(detail) = broken {
return Err(LatticeGeometryError::Internal { detail });
}
for c in counts.iter_mut().skip(1) {
*c *= 2u32;
}
counts[0] = Integer::from(1);
Ok(counts)
}
pub fn closest_vector(
&self,
target: &[Rational],
) -> Result<LatticeVector, LatticeGeometryError> {
self.closest_vector_with_budget(target, DEFAULT_ENUM_NODE_BUDGET)
}
pub fn closest_vector_with_budget(
&self,
target: &[Rational],
budget: u64,
) -> Result<LatticeVector, LatticeGeometryError> {
let basis = self.basis.as_ref().ok_or(LatticeGeometryError::NoBasis)?;
let n = basis[0].len();
if target.len() != n {
return Err(LatticeGeometryError::DimensionMismatch {
expected: n,
got: target.len(),
});
}
let m = self.rank();
let rhs: Vec<Rational> = (0..m)
.map(|i| {
let mut acc = Rational::new();
for (c, t) in basis[i].iter().zip(target.iter()) {
acc += Rational::from(c * t);
}
acc
})
.collect();
let ginv = quadform::inverse(&self.gram)?;
let coords: Vec<Rational> = (0..m)
.map(|i| {
let mut acc = Rational::new();
for t in 0..m {
acc += Rational::from(&ginv[i][t] * &rhs[t]);
}
acc
})
.collect();
let (mut e, u, _) = self.reduced_enumerator(budget)?;
let u_rat: Vec<Vec<Rational>> = u
.iter()
.map(|r| r.iter().map(Rational::from).collect())
.collect();
let u_inv = quadform::inverse(&u_rat)?;
let center: Vec<Rational> = (0..m)
.map(|i| {
let mut acc = Rational::new();
for t in 0..m {
acc += Rational::from(&coords[t] * &u_inv[t][i]);
}
acc
})
.collect();
let bound = e.babai_bound(¢er);
let mut best: Option<(Vec<Integer>, Rational)> = None;
e.enumerate(&bound, Some(¢er), false, &mut |found| {
let better = match &best {
None => true,
Some((_, n)) => found.norm < *n,
};
if better {
best = Some((found.x.to_vec(), found.norm.clone()));
}
})?;
let (x, norm) = best.ok_or(LatticeGeometryError::NotPositiveDefinite { pivot: 1 })?;
let coefficients = self.lift(&x, &u);
let coordinates = self.coordinates_of(&coefficients);
Ok(LatticeVector {
coefficients,
coordinates,
norm,
})
}
pub fn hermite_invariant(&self) -> Result<f64, LatticeGeometryError> {
let min = self.minimum()?.to_f64();
let det = self.determinant().to_f64();
Ok(min / det.powf(1.0 / self.rank() as f64))
}
pub fn center_density(&self) -> Result<f64, LatticeGeometryError> {
let min = self.minimum()?.to_f64();
let det = self.determinant().to_f64();
Ok((min / 4.0).powf(self.rank() as f64 / 2.0) / det.sqrt())
}
pub fn center_density_exact(&self) -> Result<Option<Rational>, LatticeGeometryError> {
let m = self.rank();
if m % 2 != 0 {
return Ok(None);
}
let Some(root) = exact_sqrt(&self.determinant()) else {
return Ok(None);
};
let quarter_min = self.minimum()? / Rational::from(4);
let mut num = Rational::from(1);
for _ in 0..(m / 2) {
num *= &quarter_min;
}
Ok(Some(num / root))
}
pub fn packing_density(&self) -> Result<f64, LatticeGeometryError> {
Ok(self.center_density()? * unit_ball_volume(self.rank()))
}
pub fn lll_reduced(&self) -> Result<Self, LatticeGeometryError> {
if let Some(b) = &self.basis {
if b.iter().all(|r| r.iter().all(|x| *x.denom() == 1i32)) {
let int_rows: Vec<Vec<Integer>> = b
.iter()
.map(|r| r.iter().map(|x| Integer::from(x.numer())).collect())
.collect();
let reduced = lattice_reduce_rows(&int_rows)?;
return Self::from_basis(&reduced);
}
}
let (g, u) = quadform::gram_lll(&self.gram, &Rational::from((99, 100)))?;
let basis = self.basis.as_ref().map(|b| {
let m = b.len();
let n = b[0].len();
(0..m)
.map(|i| {
(0..n)
.map(|c| {
let mut acc = Rational::new();
for t in 0..m {
acc += Rational::from(&b[t][c] * &Rational::from(&u[i][t]));
}
acc
})
.collect()
})
.collect()
});
Ok(Self {
basis,
gram: g,
minimal: OnceLock::new(),
})
}
}
fn exact_sqrt(x: &Rational) -> Option<Rational> {
if *x < 0 {
return None;
}
let n = Integer::from(x.numer());
let d = Integer::from(x.denom());
let (rn, remn) = n.sqrt_rem(Integer::new());
if remn != 0 {
return None;
}
let (rd, remd) = d.sqrt_rem(Integer::new());
if remd != 0 {
return None;
}
Some(Rational::from((rn, rd)))
}
fn unit_ball_volume(m: usize) -> f64 {
let pi = std::f64::consts::PI;
let even = m % 2 == 0;
let mut v = if even { 1.0 } else { 2.0 };
let mut k = if even { 2 } else { 3 };
while k <= m {
v *= 2.0 * pi / k as f64;
k += 2;
}
v
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_ball_volumes_match_the_closed_forms() {
assert!((unit_ball_volume(1) - 2.0).abs() < 1e-12);
assert!((unit_ball_volume(2) - std::f64::consts::PI).abs() < 1e-12);
assert!((unit_ball_volume(3) - 4.0 / 3.0 * std::f64::consts::PI).abs() < 1e-12);
assert!((unit_ball_volume(4) - std::f64::consts::PI.powi(2) / 2.0).abs() < 1e-12);
assert!((unit_ball_volume(8) - std::f64::consts::PI.powi(4) / 24.0).abs() < 1e-12);
}
#[test]
fn exact_sqrt_refuses_irrationals() {
assert_eq!(exact_sqrt(&Rational::from(4)), Some(Rational::from(2)));
assert_eq!(
exact_sqrt(&Rational::from((9, 16))),
Some(Rational::from((3, 4)))
);
assert_eq!(exact_sqrt(&Rational::from(2)), None);
assert_eq!(exact_sqrt(&Rational::from((1, 3))), None);
}
}