|
1 | | -use crate::matrix_operations; |
| 1 | +use crate::{matrix_operations, numeric::cast}; |
2 | 2 |
|
3 | 3 | use super::*; |
| 4 | +use num::{Float, ToPrimitive}; |
4 | 5 | use rand; |
5 | 6 | use rand::RngExt; |
6 | 7 | use rand_distr::{Distribution, Gamma}; |
@@ -1375,6 +1376,247 @@ fn is_norm_p_projection( |
1375 | 1376 | true |
1376 | 1377 | } |
1377 | 1378 |
|
| 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 | + |
1378 | 1620 | #[test] |
1379 | 1621 | fn t_ballp_at_origin_projection() { |
1380 | 1622 | let radius = 0.8; |
@@ -1457,3 +1699,23 @@ fn t_ballp_at_xc_projection_f32() { |
1457 | 1699 | assert!((x[0] - proj_expected[0]).abs() < 1e-4_f32); |
1458 | 1700 | assert!((x[1] - proj_expected[1]).abs() < 1e-4_f32); |
1459 | 1701 | } |
| 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