#![allow(clippy::doc_markdown, clippy::many_single_char_names)]
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LambdaError {
DimensionMismatch {
covariance: usize,
ambiguities: usize,
},
Empty,
NotFinite,
NotSymmetric {
row: usize,
column: usize,
},
NotPositiveDefinite {
pivot: usize,
},
SearchExhausted {
found: usize,
},
}
impl fmt::Display for LambdaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::DimensionMismatch {
covariance,
ambiguities,
} => write!(
f,
"a covariance of {covariance} entries does not go with {ambiguities} ambiguities"
),
Self::Empty => write!(f, "there are no ambiguities to resolve"),
Self::NotFinite => write!(f, "the covariance or the float vector is not finite"),
Self::NotSymmetric { row, column } => write!(
f,
"the covariance is not symmetric between entry ({row}, {column}) and its transpose"
),
Self::NotPositiveDefinite { pivot } => write!(
f,
"the covariance is not positive definite: pivot {pivot} is not positive"
),
Self::SearchExhausted { found } => write!(
f,
"the enumeration visited {MAX_SEARCH_NODES} nodes holding {found} candidates \
without exhausting the ellipsoid"
),
}
}
}
impl std::error::Error for LambdaError {}
pub const MAX_SEARCH_NODES: usize = 1_000_000;
#[derive(Debug, Clone, PartialEq)]
pub struct Candidate {
pub ambiguities: Vec<i64>,
pub squared_norm: f64,
}
#[derive(Debug, Clone, PartialEq)]
struct Square {
n: usize,
data: Vec<f64>,
}
impl Square {
fn identity(n: usize) -> Self {
let mut data = vec![0.0; n * n];
for i in 0..n {
data[i * n + i] = 1.0;
}
Self { n, data }
}
fn at(&self, i: usize, j: usize) -> f64 {
self.data[i * self.n + j]
}
fn set(&mut self, i: usize, j: usize, v: f64) {
self.data[i * self.n + j] = v;
}
fn swap_columns(&mut self, a: usize, b: usize) {
for i in 0..self.n {
self.data.swap(i * self.n + a, i * self.n + b);
}
}
fn swap_rows(&mut self, a: usize, b: usize) {
for j in 0..self.n {
self.data.swap(a * self.n + j, b * self.n + j);
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Reduction {
n: usize,
l: Square,
d: Vec<f64>,
z: Square,
z_inverse: Square,
z_hat: Vec<f64>,
}
fn ltdl(q: &[f64], n: usize) -> Result<(Square, Vec<f64>), LambdaError> {
let mut work = q.to_vec();
let mut l = Square::identity(n);
let mut d = vec![0.0; n];
for i in (0..n).rev() {
let pivot = work[i * n + i];
if !(pivot.is_finite() && pivot > 0.0) {
return Err(LambdaError::NotPositiveDefinite { pivot: i });
}
d[i] = pivot;
for j in 0..=i {
l.set(i, j, work[i * n + j] / pivot.sqrt());
}
for j in 0..i {
let scale = l.at(i, j);
for k in 0..=j {
work[j * n + k] -= l.at(i, k) * scale;
}
}
let diagonal = l.at(i, i);
for j in 0..=i {
l.set(i, j, l.at(i, j) / diagonal);
}
}
Ok((l, d))
}
impl Reduction {
pub fn new(float: &[f64], covariance: &[f64]) -> Result<Self, LambdaError> {
let n = float.len();
if n == 0 {
return Err(LambdaError::Empty);
}
if covariance.len() != n * n {
return Err(LambdaError::DimensionMismatch {
covariance: covariance.len(),
ambiguities: n,
});
}
if !float.iter().chain(covariance).all(|v| v.is_finite()) {
return Err(LambdaError::NotFinite);
}
let scale = covariance.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
for i in 0..n {
for j in 0..i {
let asymmetry = (covariance[i * n + j] - covariance[j * n + i]).abs();
if asymmetry > 1e-9 * scale.max(f64::MIN_POSITIVE) {
return Err(LambdaError::NotSymmetric { row: i, column: j });
}
}
}
let (mut l, mut d) = ltdl(covariance, n)?;
let mut z = Square::identity(n);
let mut z_inverse = Square::identity(n);
let mut z_hat = float.to_vec();
if n >= 2 {
let mut k = n - 2;
let mut settled = k;
loop {
if k <= settled {
for i in k + 1..n {
gauss(&mut l, &mut z, &mut z_inverse, &mut z_hat, i, k);
}
}
let candidate = d[k] + l.at(k + 1, k) * l.at(k + 1, k) * d[k + 1];
if candidate < d[k + 1] {
permute(
&mut l,
&mut d,
&mut z,
&mut z_inverse,
&mut z_hat,
k,
candidate,
);
settled = k;
k = n - 2;
} else if k == 0 {
break;
} else {
k -= 1;
}
}
}
Ok(Self {
n,
l,
d,
z,
z_inverse,
z_hat,
})
}
#[must_use]
pub const fn len(&self) -> usize {
self.n
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.n == 0
}
#[must_use]
pub fn transformation(&self) -> &[f64] {
&self.z.data
}
#[must_use]
pub fn conditional_variances(&self) -> &[f64] {
&self.d
}
#[must_use]
pub fn bootstrap(&self) -> Candidate {
let mut z = vec![0_i64; self.n];
let mut conditioned = vec![0.0_f64; self.n];
for i in (0..self.n).rev() {
conditioned[i] = self.conditional_float(&z, &conditioned, i);
z[i] = round_half_away(conditioned[i]);
}
let squared_norm = self.objective(&z, &conditioned);
Candidate {
ambiguities: self.back_transform(&z),
squared_norm,
}
}
pub fn search(&self, count: usize) -> Result<Vec<Candidate>, LambdaError> {
if count == 0 {
return Ok(Vec::new());
}
let n = self.n;
let mut best: Vec<(Vec<i64>, f64)> = Vec::with_capacity(count + 1);
let mut bound = f64::INFINITY;
let mut z = vec![0_i64; n];
let mut conditioned = vec![0.0_f64; n];
let mut offset = vec![0.0_f64; n];
let mut partial = vec![0.0_f64; n + 1];
let mut step = vec![0.0_f64; n];
let stride = n + 1;
let mut projected = vec![0.0_f64; n * stride];
let mut valid = vec![n; n];
let mut budget = MAX_SEARCH_NODES;
let mut level = n - 1;
let mut deepest = level;
self.enter(
level,
stride,
&mut conditioned,
&mut projected,
&mut valid,
&offset,
);
z[level] = round_half_away(conditioned[level]);
offset[level] = float_of(z[level]) - conditioned[level];
step[level] = if offset[level] > 0.0 { -1.0 } else { 1.0 };
loop {
let Some(left) = budget.checked_sub(1) else {
return Err(LambdaError::SearchExhausted { found: best.len() });
};
budget = left;
let cost = partial[level + 1] + offset[level] * offset[level] / self.d[level];
if cost < bound {
if level > 0 {
partial[level] = cost;
level -= 1;
deepest = level;
self.enter(
level,
stride,
&mut conditioned,
&mut projected,
&mut valid,
&offset,
);
z[level] = round_half_away(conditioned[level]);
offset[level] = float_of(z[level]) - conditioned[level];
step[level] = if offset[level] > 0.0 { -1.0 } else { 1.0 };
continue;
}
best.push((z.clone(), cost));
best.sort_by(|a, b| a.1.total_cmp(&b.1));
best.truncate(count);
if best.len() == count {
bound = best[best.len() - 1].1;
}
advance(&mut z, &mut step, level);
offset[level] = float_of(z[level]) - conditioned[level];
} else {
if level == n - 1 {
break;
}
level += 1;
advance(&mut z, &mut step, level);
offset[level] = float_of(z[level]) - conditioned[level];
invalidate(&mut valid, deepest, level);
deepest = level;
}
}
Ok(best
.into_iter()
.map(|(z, squared_norm)| Candidate {
ambiguities: self.back_transform(&z),
squared_norm,
})
.collect())
}
fn enter(
&self,
i: usize,
stride: usize,
conditioned: &mut [f64],
projected: &mut [f64],
valid: &mut [usize],
offset: &[f64],
) {
let current = i + 1;
let column = &mut projected[i * stride..][..=self.n];
for j in (current..valid[i]).rev() {
column[j] = column[j + 1] + offset[j] * self.l.at(j, i);
}
valid[i] = current;
conditioned[i] = self.z_hat[i] + column[current];
}
fn conditional_float(&self, z: &[i64], conditioned: &[f64], i: usize) -> f64 {
let mut acc = self.z_hat[i];
for j in i + 1..self.n {
acc += (float_of(z[j]) - conditioned[j]) * self.l.at(j, i);
}
acc
}
fn objective(&self, z: &[i64], conditioned: &[f64]) -> f64 {
(0..self.n)
.map(|i| {
let r = float_of(z[i]) - conditioned[i];
r * r / self.d[i]
})
.sum()
}
fn back_transform(&self, z: &[i64]) -> Vec<i64> {
(0..self.n)
.map(|i| {
let value: f64 = (0..self.n)
.map(|j| self.z_inverse.at(j, i) * float_of(z[j]))
.sum();
round_half_away(value)
})
.collect()
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn round_half_away(x: f64) -> i64 {
let r = x.round();
if r >= i64::MAX as f64 {
i64::MAX
} else if r <= i64::MIN as f64 {
i64::MIN
} else {
r as i64
}
}
#[allow(clippy::cast_precision_loss)]
fn float_of(x: i64) -> f64 {
x as f64
}
fn advance(z: &mut [i64], step: &mut [f64], level: usize) {
z[level] += round_half_away(step[level]);
step[level] = -step[level] - step[level].signum();
}
fn invalidate(valid: &mut [usize], deepest: usize, level: usize) {
for entry in &mut valid[deepest..level] {
*entry = level + 1;
}
for entry in valid[..deepest].iter_mut().rev() {
if *entry > level {
break;
}
*entry = level + 1;
}
}
fn gauss(
l: &mut Square,
z: &mut Square,
z_inverse: &mut Square,
z_hat: &mut [f64],
i: usize,
j: usize,
) {
let mu = float_of(round_half_away(l.at(i, j)));
if mu == 0.0 {
return;
}
let n = l.n;
for k in i..n {
l.set(k, j, l.at(k, j) - mu * l.at(k, i));
}
for k in 0..n {
z.set(k, j, z.at(k, j) - mu * z.at(k, i));
z_inverse.set(i, k, z_inverse.at(i, k) + mu * z_inverse.at(j, k));
}
z_hat[j] -= mu * z_hat[i];
}
fn permute(
l: &mut Square,
d: &mut [f64],
z: &mut Square,
z_inverse: &mut Square,
z_hat: &mut [f64],
k: usize,
delta: f64,
) {
let n = l.n;
let eta = d[k] / delta;
let lambda = d[k + 1] * l.at(k + 1, k) / delta;
d[k] = eta * d[k + 1];
d[k + 1] = delta;
for j in 0..k {
let (a, b) = (l.at(k, j), l.at(k + 1, j));
l.set(k, j, (-l.at(k + 1, k)).mul_add(a, b));
l.set(k + 1, j, eta.mul_add(a, lambda * b));
}
l.set(k + 1, k, lambda);
for i in k + 2..n {
let (a, b) = (l.at(i, k), l.at(i, k + 1));
l.set(i, k, b);
l.set(i, k + 1, a);
}
z.swap_columns(k, k + 1);
z_inverse.swap_rows(k, k + 1);
z_hat.swap(k, k + 1);
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use proptest::prelude::*;
use super::*;
fn from_factors(r: &Reduction) -> Vec<f64> {
let n = r.n;
let mut out = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
out[i * n + j] = (0..n).map(|k| r.l.at(k, i) * r.d[k] * r.l.at(k, j)).sum();
}
}
out
}
fn transformed(q: &[f64], r: &Reduction) -> Vec<f64> {
let n = r.n;
let mut out = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
out[i * n + j] = (0..n)
.map(|a| {
r.z.at(a, i) * (0..n).map(|b| q[a * n + b] * r.z.at(b, j)).sum::<f64>()
})
.sum();
}
}
out
}
fn elongated() -> (Vec<f64>, Vec<f64>) {
let q = vec![
6.290, 5.978, 0.544, 5.978, 6.292, 2.340, 0.544, 2.340, 6.288,
];
let float = vec![5.45, 3.10, 2.97];
(float, q)
}
#[test]
fn the_factorisation_reproduces_the_matrix() {
let (_, q) = elongated();
let (l, d) = ltdl(&q, 3).unwrap();
for (i, &di) in d.iter().enumerate() {
assert!((l.at(i, i) - 1.0).abs() < 1e-12, "unit diagonal at {i}");
assert!(di > 0.0, "d[{i}] = {di}");
for j in i + 1..3 {
assert!(l.at(i, j).abs() < 1e-15, "upper entry ({i}, {j})");
}
}
let mut worst = 0.0_f64;
for i in 0..3 {
for j in 0..3 {
let rebuilt: f64 = (0..3).map(|k| l.at(k, i) * d[k] * l.at(k, j)).sum();
worst = worst.max((rebuilt - q[i * 3 + j]).abs());
}
}
assert!(worst < 1e-12, "LᵀDL residual {worst:.3e}");
}
#[test]
fn the_reduction_is_unimodular_consistent_and_bounded() {
let (float, q) = elongated();
let r = Reduction::new(&float, &q).unwrap();
for v in r.transformation() {
assert!((v - v.round()).abs() < 1e-12, "non-integer Z entry {v}");
}
let z = |i: usize, j: usize| r.z.at(i, j);
let det = z(0, 0) * (z(1, 1) * z(2, 2) - z(1, 2) * z(2, 1))
- z(0, 1) * (z(1, 0) * z(2, 2) - z(1, 2) * z(2, 0))
+ z(0, 2) * (z(1, 0) * z(2, 1) - z(1, 1) * z(2, 0));
assert!((det.abs() - 1.0).abs() < 1e-9, "det Z = {det}");
for i in 0..3 {
for j in 0..3 {
let entry: f64 = (0..3).map(|k| r.z.at(i, k) * r.z_inverse.at(k, j)).sum();
let want = f64::from(u8::from(i == j));
assert!((entry - want).abs() < 1e-9, "ZZ⁻¹ ({i}, {j}) = {entry}");
}
}
let want = transformed(&q, &r);
let got = from_factors(&r);
let worst = want
.iter()
.zip(&got)
.fold(0.0_f64, |m, (a, b)| m.max((a - b).abs()));
assert!(worst < 1e-9, "ZᵀQZ against L̄ᵀD̄L̄: {worst:.3e}");
for i in 0..3 {
for j in 0..i {
assert!(
r.l.at(i, j).abs() <= 0.5 + 1e-12,
"L̄({i}, {j}) = {}",
r.l.at(i, j)
);
}
}
}
#[test]
fn the_reduction_flattens_the_conditional_variances() {
let (float, q) = elongated();
let (_, before) = ltdl(&q, 3).unwrap();
let r = Reduction::new(&float, &q).unwrap();
let after = r.conditional_variances();
let spread = |d: &[f64]| {
let hi = d.iter().fold(f64::MIN, |m, v| m.max(*v));
let lo = d.iter().fold(f64::MAX, |m, v| m.min(*v));
hi / lo
};
let (was, now) = (spread(&before), spread(after));
assert!(
now < was / 5.0,
"spread {was:.4e} -> {now:.4e} is not a decorrelation"
);
assert!((was - 6.9977e1).abs() < 1e-2, "before {was:.4e}");
assert!((now - 6.8852e0).abs() < 1e-3, "after {now:.4e}");
}
#[test]
fn a_diagonal_covariance_resolves_to_the_rounded_float() {
let float = vec![1.4, -2.6, 0.5, 7.49];
let q = vec![
0.20, 0.0, 0.0, 0.0, 0.0, 0.11, 0.0, 0.0, 0.0, 0.0, 0.37, 0.0, 0.0, 0.0, 0.0, 0.05,
];
let r = Reduction::new(&float, &q).unwrap();
let best = r.search(1).unwrap();
assert_eq!(best[0].ambiguities, vec![1, -3, 1, 7]);
assert_eq!(r.bootstrap().ambiguities, vec![1, -3, 1, 7]);
}
#[test]
fn an_integer_float_vector_resolves_to_itself() {
let (_, q) = elongated();
let float = vec![4.0, -7.0, 11.0];
let r = Reduction::new(&float, &q).unwrap();
let best = r.search(2).unwrap();
assert_eq!(best[0].ambiguities, vec![4, -7, 11]);
assert!(
best[0].squared_norm.abs() < 1e-18,
"{}",
best[0].squared_norm
);
assert!(
best[1].squared_norm > 0.0,
"the second candidate is the first"
);
}
#[test]
fn the_reported_objective_is_the_objective_in_the_original_problem() {
let (float, q) = elongated();
let r = Reduction::new(&float, &q).unwrap();
let candidates = r.search(5).unwrap();
assert_eq!(candidates.len(), 5);
let inverse = invert3(&q);
let mut previous = f64::NEG_INFINITY;
for c in &candidates {
let residual: Vec<f64> = (0..3)
.map(|i| float_of(c.ambiguities[i]) - float[i])
.collect();
let quadratic: f64 = (0..3)
.map(|i| {
residual[i]
* (0..3)
.map(|j| inverse[i * 3 + j] * residual[j])
.sum::<f64>()
})
.sum();
assert!(
(quadratic - c.squared_norm).abs() < 1e-9 * quadratic.max(1.0),
"{:?}: {quadratic} vs {}",
c.ambiguities,
c.squared_norm
);
assert!(c.squared_norm >= previous, "candidates out of order");
previous = c.squared_norm;
}
}
fn invert3(q: &[f64]) -> Vec<f64> {
let m = |i: usize, j: usize| q[i * 3 + j];
let c = [
m(1, 1) * m(2, 2) - m(1, 2) * m(2, 1),
m(0, 2) * m(2, 1) - m(0, 1) * m(2, 2),
m(0, 1) * m(1, 2) - m(0, 2) * m(1, 1),
m(1, 2) * m(2, 0) - m(1, 0) * m(2, 2),
m(0, 0) * m(2, 2) - m(0, 2) * m(2, 0),
m(0, 2) * m(1, 0) - m(0, 0) * m(1, 2),
m(1, 0) * m(2, 1) - m(1, 1) * m(2, 0),
m(0, 1) * m(2, 0) - m(0, 0) * m(2, 1),
m(0, 0) * m(1, 1) - m(0, 1) * m(1, 0),
];
let det = m(0, 0) * c[0] + m(0, 1) * c[3] + m(0, 2) * c[6];
c.iter().map(|v| v / det).collect()
}
#[test]
fn a_covariance_it_cannot_use_is_refused() {
assert_eq!(Reduction::new(&[], &[]), Err(LambdaError::Empty));
assert_eq!(
Reduction::new(&[1.0, 2.0], &[1.0, 0.0, 0.0]),
Err(LambdaError::DimensionMismatch {
covariance: 3,
ambiguities: 2,
})
);
assert_eq!(
Reduction::new(&[f64::NAN], &[1.0]),
Err(LambdaError::NotFinite)
);
assert_eq!(
Reduction::new(&[1.0, 2.0], &[1.0, 0.5, 0.4, 1.0]),
Err(LambdaError::NotSymmetric { row: 1, column: 0 })
);
assert_eq!(
Reduction::new(&[1.0, 2.0], &[1.0, 2.0, 2.0, 1.0]),
Err(LambdaError::NotPositiveDefinite { pivot: 0 })
);
assert!(r_messages_are_distinct());
}
fn r_messages_are_distinct() -> bool {
let messages: Vec<String> = [
LambdaError::Empty,
LambdaError::DimensionMismatch {
covariance: 3,
ambiguities: 2,
},
LambdaError::NotFinite,
LambdaError::NotSymmetric { row: 1, column: 0 },
LambdaError::NotPositiveDefinite { pivot: 0 },
LambdaError::SearchExhausted { found: 1 },
]
.iter()
.map(ToString::to_string)
.collect();
let distinct: std::collections::HashSet<&String> = messages.iter().collect();
distinct.len() == messages.len() && messages.iter().all(|m| !m.is_empty())
}
#[test]
fn a_zero_candidate_search_is_empty() {
let (float, q) = elongated();
assert_eq!(
Reduction::new(&float, &q).unwrap().search(0).unwrap().len(),
0
);
}
#[test]
fn a_zero_variance_is_refused_rather_than_factorised() {
assert_eq!(
Reduction::new(&[1.0, 2.0], &[1.0, 0.0, 0.0, 0.0]),
Err(LambdaError::NotPositiveDefinite { pivot: 1 })
);
assert_eq!(
Reduction::new(&[1.0, 2.0], &[0.0, 0.0, 0.0, 0.0]),
Err(LambdaError::NotPositiveDefinite { pivot: 1 })
);
}
#[test]
fn the_bootstrapped_objective_is_the_quadratic_form_at_the_rounded_float() {
let float = [1.4, -2.6, 0.5, 7.49];
let variance = [0.20, 0.11, 0.37, 0.05];
let mut q = vec![0.0; 16];
for (i, v) in variance.iter().enumerate() {
q[i * 4 + i] = *v;
}
let candidate = Reduction::new(&float, &q).unwrap().bootstrap();
assert_eq!(candidate.ambiguities, vec![1, -3, 1, 7]);
let want: f64 = (0..4)
.map(|i| {
let residual = float[i].round() - float[i];
residual * residual / variance[i]
})
.sum();
assert!((want - 7.732_221_130_221_133).abs() < 1e-15, "{want}");
assert!(
(candidate.squared_norm - want).abs() < 1e-15,
"{} vs {want}",
candidate.squared_norm
);
}
#[test]
fn the_enumeration_walks_outwards_from_the_conditional_float() {
let (_, q) = elongated();
let r = Reduction::new(&[4.0, -7.0, 11.0], &q).unwrap();
assert_eq!(r.len(), 3);
let found = r.search(14).unwrap();
let want = [
(0, vec![4, -7, 11]),
(1, vec![5, -6, 11]),
(2, vec![3, -8, 11]),
(3, vec![5, -6, 12]),
(4, vec![3, -8, 10]),
(13, vec![4, -8, 8]),
];
for (index, want) in &want {
assert_eq!(&found[*index].ambiguities, want, "candidate {index}");
}
for pair in [(1, 2), (3, 4), (11, 12)] {
assert_eq!(
found[pair.0].squared_norm.to_bits(),
found[pair.1].squared_norm.to_bits(),
"candidates {pair:?} must tie: {} vs {}",
found[pair.0].squared_norm,
found[pair.1].squared_norm
);
}
}
#[test]
fn a_single_ambiguity_resolves() {
let r = Reduction::new(&[3.4], &[0.25]).unwrap();
assert_eq!(r.len(), 1);
assert!(!r.is_empty());
assert_eq!(r.search(1).unwrap()[0].ambiguities, vec![3]);
assert!((r.search(1).unwrap()[0].squared_norm - 0.64).abs() < 1e-12);
}
fn recomputed_search(r: &Reduction, count: usize) -> Vec<Candidate> {
let n = r.n;
let mut budget = MAX_SEARCH_NODES;
let mut best: Vec<(Vec<i64>, f64)> = Vec::with_capacity(count + 1);
let mut bound = f64::INFINITY;
let mut z = vec![0_i64; n];
let mut conditioned = vec![0.0_f64; n];
let mut partial = vec![0.0_f64; n + 1];
let mut step = vec![0.0_f64; n];
let mut level = n - 1;
conditioned[level] = r.conditional_float(&z, &conditioned, level);
z[level] = round_half_away(conditioned[level]);
step[level] = if float_of(z[level]) > conditioned[level] {
-1.0
} else {
1.0
};
loop {
assert!(budget > 0, "the recomputing oracle outran the node budget");
budget -= 1;
let residual = float_of(z[level]) - conditioned[level];
let cost = partial[level + 1] + residual * residual / r.d[level];
if cost < bound {
if level > 0 {
partial[level] = cost;
level -= 1;
conditioned[level] = r.conditional_float(&z, &conditioned, level);
z[level] = round_half_away(conditioned[level]);
step[level] = if float_of(z[level]) > conditioned[level] {
-1.0
} else {
1.0
};
continue;
}
best.push((z.clone(), cost));
best.sort_by(|a, b| a.1.total_cmp(&b.1));
best.truncate(count);
if best.len() == count {
bound = best[best.len() - 1].1;
}
advance(&mut z, &mut step, level);
} else {
if level == n - 1 {
break;
}
level += 1;
advance(&mut z, &mut step, level);
}
}
best.into_iter()
.map(|(z, squared_norm)| Candidate {
ambiguities: r.back_transform(&z),
squared_norm,
})
.collect()
}
fn covariance_from(entries: &[f64], n: usize, elongation: f64) -> Vec<f64> {
let mut b = vec![0.0_f64; n * n];
b.copy_from_slice(&entries[..n * n]);
let shared = &entries[n * n..n * n + n];
for i in 0..n {
for j in 0..n {
b[i * n + j] += elongation * shared[j];
}
}
let mut q = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
q[i * n + j] = (0..n).map(|k| b[i * n + k] * b[j * n + k]).sum();
}
q[i * n + i] += 0.05;
}
q
}
proptest! {
#[test]
fn the_carried_projections_reproduce_the_recomputed_search(
n in 2_usize..=10,
entries in prop::collection::vec(-1.0_f64..1.0, 110),
floats in prop::collection::vec(-20.0_f64..20.0, 10),
count in 1_usize..=6,
) {
let q = covariance_from(&entries, n, 2.5);
let Ok(r) = Reduction::new(&floats[..n], &q) else {
return Ok(());
};
let got = r.search(count).unwrap();
let want = recomputed_search(&r, count);
prop_assert_eq!(got.len(), want.len(), "candidate count");
for (i, (a, b)) in got.iter().zip(&want).enumerate() {
prop_assert_eq!(
&a.ambiguities, &b.ambiguities,
"candidate {} differs: {:?} against {:?}", i, a, b
);
let scale = b.squared_norm.abs().max(1.0);
prop_assert!(
(a.squared_norm - b.squared_norm).abs() < 1e-9 * scale,
"candidate {} objective {} against {}", i, a.squared_norm, b.squared_norm
);
}
}
}
#[test]
fn the_node_budget_is_reachable_and_refuses_rather_than_truncates() {
let tied = |n: usize| {
let mut q = vec![0.0_f64; n * n];
for i in 0..n {
q[i * n + i] = 1.0;
}
Reduction::new(&vec![0.5_f64; n], &q).unwrap().search(1)
};
assert_eq!(tied(18).unwrap().len(), 1);
assert_eq!(tied(19), Err(LambdaError::SearchExhausted { found: 1 }));
}
#[test]
#[ignore = "a measurement, not a check: see the table in the doc comment"]
#[expect(
clippy::cast_precision_loss,
reason = "a draw in [-1, 1) and a repetition count, neither near 2^53"
)]
fn the_reduced_conditioning_pays_above_thirty_ambiguities() {
fn splitmix(s: &mut u64) -> f64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
let u = z ^ (z >> 31);
(u >> 11) as f64 / (1_u64 << 53) as f64 * 2.0 - 1.0
}
for n in [5_usize, 10, 20, 30, 40, 50, 60, 70, 80] {
let mut s = 0x5EED_0000_0000_0001_u64;
let entries: Vec<f64> = (0..n * n + n).map(|_| splitmix(&mut s)).collect();
let q = covariance_from(&entries, n, 0.6);
let floats: Vec<f64> = (0..n).map(|_| splitmix(&mut s) * 20.0).collect();
let r = Reduction::new(&floats, &q).unwrap();
if r.search(1).is_err() {
println!("n={n:3} the ellipsoid outgrew the node budget");
break;
}
let reps = 20_usize.max(2000 / n);
let mut ta = std::time::Duration::ZERO;
let mut tb = std::time::Duration::ZERO;
let mut rounds = 0;
for _ in 0..4 {
let t = std::time::Instant::now();
for _ in 0..reps {
std::hint::black_box(r.search(1).unwrap());
}
ta += t.elapsed();
let t = std::time::Instant::now();
for _ in 0..reps {
std::hint::black_box(recomputed_search(&r, 1));
}
tb += t.elapsed();
rounds += reps;
}
let a = ta.as_secs_f64() / rounds as f64;
let b = tb.as_secs_f64() / rounds as f64;
println!(
"n={n:3} new {:9.3} us old {:9.3} us x{:.2}",
a * 1e6,
b * 1e6,
b / a
);
}
}
}