Skip to content

Commit 5d89d2e

Browse files
committed
tighter unit testing (f36/f64)
1 parent 857a131 commit 5d89d2e

2 files changed

Lines changed: 267 additions & 5 deletions

File tree

src/constraints/epigraph_squared_norm.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ where
9595

9696
let cubic_poly_roots = roots::find_roots_cubic(a3, a2, a1, a0);
9797

98-
let root_tol = cast::<T>(1e-6);
98+
let root_tol = cast::<T>(10.0) * num::Float::sqrt(T::epsilon());
9999
let mut right_root: Option<T> = None;
100100

101101
// Pick the first admissible real root
@@ -117,7 +117,7 @@ where
117117

118118
// Newton refinement
119119
let newton_max_iters: usize = 5;
120-
let newton_eps = cast::<T>(1e-14);
120+
let newton_eps = cast::<T>(10.0) * T::epsilon();
121121

122122
for _ in 0..newton_max_iters {
123123
let zsol_sq = zsol * zsol;
@@ -130,7 +130,7 @@ where
130130

131131
let dp_z = cast::<T>(3.0) * a3 * zsol_sq + cast::<T>(2.0) * a2 * zsol + a1;
132132
assert!(
133-
num::Float::abs(dp_z) > cast::<T>(1e-15),
133+
num::Float::abs(dp_z) > cast::<T>(10.0) * T::epsilon(),
134134
"EpigraphSquaredNorm::project: Newton derivative too small"
135135
);
136136

@@ -141,7 +141,7 @@ where
141141
let scaling = cast::<T>(1.0) + cast::<T>(2.0) * (right_root - t);
142142

143143
assert!(
144-
num::Float::abs(scaling) > cast::<T>(1e-15),
144+
num::Float::abs(scaling) > cast::<T>(10.0) * T::epsilon(),
145145
"EpigraphSquaredNorm::project: scaling factor too small"
146146
);
147147

src/constraints/tests.rs

Lines changed: 263 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use crate::matrix_operations;
1+
use crate::{matrix_operations, numeric::cast};
22

33
use super::*;
4+
use num::{Float, ToPrimitive};
45
use rand;
56
use rand::RngExt;
67
use rand_distr::{Distribution, Gamma};
@@ -1375,6 +1376,247 @@ fn is_norm_p_projection(
13751376
true
13761377
}
13771378

1379+
fn is_norm_p_projection_with_tol(
1380+
x: &[f64],
1381+
x_candidate_proj: &[f64],
1382+
p: f64,
1383+
radius: f64,
1384+
sample_points: usize,
1385+
feasibility_tol: f64,
1386+
inner_prod_tol: f64,
1387+
) -> bool {
1388+
let n = x.len();
1389+
assert_eq!(n, x_candidate_proj.len());
1390+
1391+
let norm_proj = x_candidate_proj
1392+
.iter()
1393+
.map(|xi| xi.abs().powf(p))
1394+
.sum::<f64>()
1395+
.powf(1.0 / p);
1396+
if norm_proj > radius + feasibility_tol {
1397+
return false;
1398+
}
1399+
1400+
let e: Vec<f64> = x
1401+
.iter()
1402+
.zip(x_candidate_proj.iter())
1403+
.map(|(xi, yi)| xi - yi)
1404+
.collect();
1405+
let samples = sample_lp_sphere(sample_points, n, p, radius);
1406+
for xi in samples.iter() {
1407+
let w: Vec<f64> = x_candidate_proj
1408+
.iter()
1409+
.zip(xi.iter())
1410+
.map(|(xproj_i, xi_i)| xproj_i - xi_i)
1411+
.collect();
1412+
let inner = matrix_operations::inner_product(&w, &e);
1413+
if inner < -inner_prod_tol {
1414+
return false;
1415+
}
1416+
}
1417+
true
1418+
}
1419+
1420+
fn as_f64_vec<T: ToPrimitive>(x: &[T]) -> Vec<f64> {
1421+
x.iter()
1422+
.map(|xi| {
1423+
xi.to_f64()
1424+
.expect("test float values must be convertible to f64")
1425+
})
1426+
.collect()
1427+
}
1428+
1429+
fn lp_norm_generic<T: Float>(x: &[T], p: T) -> T {
1430+
x.iter()
1431+
.map(|xi| xi.abs().powf(p))
1432+
.fold(T::zero(), |sum, xi| sum + xi)
1433+
.powf(T::one() / p)
1434+
}
1435+
1436+
fn random_vec<T: Float>(rng: &mut impl rand::Rng, len: usize, lower: f64, upper: f64) -> Vec<T> {
1437+
(0..len)
1438+
.map(|_| cast::<T>(rng.random_range(lower..upper)))
1439+
.collect()
1440+
}
1441+
1442+
fn run_ballp_random_properties<T>()
1443+
where
1444+
T: Float + ToPrimitive,
1445+
{
1446+
let mut rng = rand::rng();
1447+
let solver_tol = if T::epsilon() > cast::<T>(1e-10) {
1448+
cast::<T>(1e-5)
1449+
} else {
1450+
cast::<T>(1e-12)
1451+
};
1452+
let feasibility_tol = if T::epsilon() > cast::<T>(1e-10) {
1453+
cast::<T>(5e-3)
1454+
} else {
1455+
cast::<T>(1e-8)
1456+
};
1457+
let idempotence_tol = if T::epsilon() > cast::<T>(1e-10) {
1458+
cast::<T>(2e-4)
1459+
} else {
1460+
cast::<T>(1e-10)
1461+
};
1462+
let inner_prod_tol = if T::epsilon() > cast::<T>(1e-10) {
1463+
5e-3
1464+
} else {
1465+
1e-8
1466+
};
1467+
1468+
for &(dim, p_f64, radius_f64, with_center) in &[
1469+
(3_usize, 1.7_f64, 1.1_f64, false),
1470+
(4_usize, 2.5_f64, 0.9_f64, true),
1471+
(5_usize, 3.4_f64, 1.4_f64, true),
1472+
] {
1473+
for _ in 0..40 {
1474+
let center = with_center.then(|| random_vec::<T>(&mut rng, dim, -1.5, 1.5));
1475+
let mut x = random_vec::<T>(&mut rng, dim, -4.0, 4.0);
1476+
let x_before = x.clone();
1477+
let p = cast::<T>(p_f64);
1478+
let radius = cast::<T>(radius_f64);
1479+
let ball = BallP::new(center.as_deref(), radius, p, solver_tol, 300);
1480+
ball.project(&mut x);
1481+
1482+
let shifted_projection: Vec<T> = if let Some(center) = center.as_ref() {
1483+
x.iter()
1484+
.zip(center.iter())
1485+
.map(|(xi, ci)| *xi - *ci)
1486+
.collect()
1487+
} else {
1488+
x.clone()
1489+
};
1490+
let proj_norm = lp_norm_generic(&shifted_projection, p);
1491+
assert!(
1492+
proj_norm <= radius + feasibility_tol,
1493+
"projected point is not feasible for BallP"
1494+
);
1495+
1496+
let mut reproj = x.clone();
1497+
ball.project(&mut reproj);
1498+
let max_reproj_diff = reproj
1499+
.iter()
1500+
.zip(x.iter())
1501+
.fold(T::zero(), |acc, (a, b)| acc.max((*a - *b).abs()));
1502+
assert!(
1503+
max_reproj_diff <= idempotence_tol,
1504+
"BallP projection is not idempotent within tolerance"
1505+
);
1506+
1507+
let shifted_x_before: Vec<f64> = if let Some(center) = center.as_ref() {
1508+
x_before
1509+
.iter()
1510+
.zip(center.iter())
1511+
.map(|(xi, ci)| {
1512+
(*xi - *ci)
1513+
.to_f64()
1514+
.expect("test float values must be convertible to f64")
1515+
})
1516+
.collect()
1517+
} else {
1518+
as_f64_vec(&x_before)
1519+
};
1520+
let shifted_projection_f64 = as_f64_vec(&shifted_projection);
1521+
assert!(
1522+
is_norm_p_projection_with_tol(
1523+
&shifted_x_before,
1524+
&shifted_projection_f64,
1525+
p_f64,
1526+
radius_f64,
1527+
500,
1528+
feasibility_tol
1529+
.to_f64()
1530+
.expect("test float values must be convertible to f64"),
1531+
inner_prod_tol,
1532+
),
1533+
"BallP projection failed sampled optimality check"
1534+
);
1535+
}
1536+
}
1537+
}
1538+
1539+
fn run_epigraph_squared_norm_random_properties<T>()
1540+
where
1541+
T: Float + roots::FloatType + std::iter::Sum<T> + ToPrimitive,
1542+
{
1543+
let mut rng = rand::rng();
1544+
let feasibility_tol = if T::epsilon() > cast::<T>(1e-10) {
1545+
cast::<T>(2e-4)
1546+
} else {
1547+
cast::<T>(1e-10)
1548+
};
1549+
let idempotence_tol = if T::epsilon() > cast::<T>(1e-10) {
1550+
cast::<T>(2e-4)
1551+
} else {
1552+
cast::<T>(1e-10)
1553+
};
1554+
let vi_tol = if T::epsilon() > cast::<T>(1e-10) {
1555+
2e-3
1556+
} else {
1557+
1e-8
1558+
};
1559+
let epi = EpigraphSquaredNorm::new();
1560+
1561+
for dim in 2..=5 {
1562+
for _ in 0..50 {
1563+
let mut x = random_vec::<T>(&mut rng, dim, -3.0, 3.0);
1564+
x.push(cast::<T>(rng.random_range(-2.0..4.0)));
1565+
let x_before = as_f64_vec(&x);
1566+
1567+
epi.project(&mut x);
1568+
1569+
let z = &x[..dim];
1570+
let t = x[dim];
1571+
let norm_z_sq = matrix_operations::norm2_squared(z);
1572+
assert!(
1573+
norm_z_sq <= t + feasibility_tol,
1574+
"Epigraph projection is not feasible"
1575+
);
1576+
1577+
let mut reproj = x.clone();
1578+
epi.project(&mut reproj);
1579+
let max_reproj_diff = reproj
1580+
.iter()
1581+
.zip(x.iter())
1582+
.fold(T::neg_infinity(), |acc, (a, b)| {
1583+
acc.max(num::Float::abs(*a - *b))
1584+
});
1585+
assert!(
1586+
max_reproj_diff <= idempotence_tol,
1587+
"Epigraph projection is not idempotent within tolerance"
1588+
);
1589+
1590+
let proj_f64 = as_f64_vec(&x);
1591+
let residual: Vec<f64> = x_before
1592+
.iter()
1593+
.zip(proj_f64.iter())
1594+
.map(|(xb, xp)| xb - xp)
1595+
.collect();
1596+
1597+
for _ in 0..150 {
1598+
let z_feasible: Vec<f64> = (0..dim)
1599+
.map(|_| rng.random_range(-3.0..3.0))
1600+
.collect();
1601+
let norm_z_sq_feasible = z_feasible.iter().map(|zi| zi * zi).sum::<f64>();
1602+
let t_feasible = norm_z_sq_feasible + rng.random_range(0.0..3.0);
1603+
let mut y = z_feasible;
1604+
y.push(t_feasible);
1605+
let diff: Vec<f64> = proj_f64
1606+
.iter()
1607+
.zip(y.iter())
1608+
.map(|(xp, yi)| xp - yi)
1609+
.collect();
1610+
let inner = matrix_operations::inner_product(&diff, &residual);
1611+
assert!(
1612+
inner >= -vi_tol,
1613+
"Epigraph projection failed sampled variational inequality"
1614+
);
1615+
}
1616+
}
1617+
}
1618+
}
1619+
13781620
#[test]
13791621
fn t_ballp_at_origin_projection() {
13801622
let radius = 0.8;
@@ -1457,3 +1699,23 @@ fn t_ballp_at_xc_projection_f32() {
14571699
assert!((x[0] - proj_expected[0]).abs() < 1e-4_f32);
14581700
assert!((x[1] - proj_expected[1]).abs() < 1e-4_f32);
14591701
}
1702+
1703+
#[test]
1704+
fn t_ballp_random_properties_f64() {
1705+
run_ballp_random_properties::<f64>();
1706+
}
1707+
1708+
#[test]
1709+
fn t_ballp_random_properties_f32() {
1710+
run_ballp_random_properties::<f32>();
1711+
}
1712+
1713+
#[test]
1714+
fn t_epigraph_squared_norm_random_properties_f64() {
1715+
run_epigraph_squared_norm_random_properties::<f64>();
1716+
}
1717+
1718+
#[test]
1719+
fn t_epigraph_squared_norm_random_properties_f32() {
1720+
run_epigraph_squared_norm_random_properties::<f32>();
1721+
}

0 commit comments

Comments
 (0)