Skip to content

Commit 7b09cb1

Browse files
committed
Epigraph of sq. Eucl. norm: improvements
About: - algorithmic improvements for better num. stability - better documentation
1 parent 7e9b626 commit 7b09cb1

File tree

4 files changed

+107
-303
lines changed

4 files changed

+107
-303
lines changed

src/constraints/epigraph_squared_norm.rs

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,84 +3,157 @@ use crate::matrix_operations;
33
use super::Constraint;
44

55
#[derive(Copy, Clone, Default)]
6-
/// The epigraph of the squared Eucliden norm is a set of the form
7-
/// $X = \\{x = (z, t) \in \mathbb{R}^{n}\times \mathbb{R} {}:{} \\|z\\|^2 \leq t \\}.$
6+
/// The epigraph of the squared Euclidean norm, that is,
7+
/// $$
8+
/// X = \{x = (z, t) \in \mathbb{R}^{n}\times \mathbb{R} : \|z\|_2^2 \le t \}.
9+
/// $$
10+
///
11+
/// A point is represented by a slice `x` whose last entry is the scalar
12+
/// component `t`, while the preceding entries form the vector component `z`.
813
pub struct EpigraphSquaredNorm {}
914

1015
impl EpigraphSquaredNorm {
1116
/// Create a new instance of the epigraph of the squared norm.
1217
///
1318
/// Note that you do not need to specify the dimension.
19+
#[must_use]
1420
pub fn new() -> Self {
1521
EpigraphSquaredNorm {}
1622
}
1723
}
1824

1925
impl Constraint for EpigraphSquaredNorm {
20-
///Project on the epigraph of the squared Euclidean norm.
26+
/// Project on the epigraph of the squared Euclidean norm.
2127
///
22-
/// The projection is computed as detailed
23-
/// [here](https://mathematix.wordpress.com/2017/05/02/projection-on-the-epigraph-of-the-squared-euclidean-norm/).
28+
/// Let the input be represented as $(z,t)$, where `z` is the vector formed
29+
/// by the first `x.len() - 1` entries of `x`, and `t` is the last entry.
30+
/// This method computes the Euclidean projection of $(z,t)$ onto
31+
///
32+
/// $$
33+
/// \operatorname{epi}\|\cdot\|_2^2
34+
/// =
35+
/// \{(u,s) \in \mathbb{R}^n \times \mathbb{R} : \|u\|_2^2 \le s\}.
36+
/// $$
37+
///
38+
/// If the point is already feasible, that is, if
39+
///
40+
/// $$
41+
/// \|z\|_2^2 \le t,
42+
/// $$
43+
///
44+
/// then the input is left unchanged.
45+
///
46+
/// Otherwise, the projection is computed using the methodology described
47+
/// [here](https://mathematix.wordpress.com/2017/05/02/projection-on-the-epigraph-of-the-squared-euclidean-norm/):
48+
///
49+
/// 1. Eliminate the projected vector variable to obtain a cubic equation
50+
/// in the projected scalar variable.
51+
/// 2. Compute the real roots of that cubic.
52+
/// 3. Select the admissible root that is consistent with the projection
53+
/// formula.
54+
/// 4. Refine the selected root with a few Newton iterations.
55+
/// 5. Recover the projected vector component using the refined root.
2456
///
2557
/// ## Arguments
26-
/// - `x`: The given vector $x$ is updated with the projection on the set
58+
///
59+
/// - `x`: The given vector `x` is updated with the projection on the set
60+
///
61+
/// ## Panics
62+
///
63+
/// Panics if:
64+
///
65+
/// - `x.len() < 2`,
66+
/// - no admissible real root is found,
67+
/// - the Newton derivative becomes too small,
68+
/// - the final scaling factor is numerically singular.
2769
///
2870
/// ## Example
2971
///
3072
/// ```rust
3173
/// use optimization_engine::constraints::*;
3274
///
3375
/// let epi = EpigraphSquaredNorm::new();
34-
/// let mut x = [1., 2., 3., 4.];
76+
///
77+
/// // Here, z = [1., 2., 3.] and t = 4.
78+
/// let mut x = [1., 2., 3., 4.];
79+
///
3580
/// epi.project(&mut x);
3681
/// ```
3782
fn project(&self, x: &mut [f64]) {
83+
assert!(
84+
x.len() >= 2,
85+
"EpigraphSquaredNorm::project requires x.len() >= 2"
86+
);
87+
3888
let nx = x.len() - 1;
39-
assert!(nx > 0, "x must have a length of at least 2");
40-
let z: &[f64] = &x[..nx];
41-
let t: f64 = x[nx];
89+
let z = &x[..nx];
90+
let t = x[nx];
4291
let norm_z_sq = matrix_operations::norm2_squared(z);
92+
93+
// Already feasible
4394
if norm_z_sq <= t {
4495
return;
4596
}
4697

47-
let theta = 1. - 2. * t;
48-
let a3 = 4.;
49-
let a2 = 4. * theta;
98+
// Cubic:
99+
// 4 r^3 + 4 theta r^2 + theta^2 r - ||z||^2 = 0
100+
let theta = 1.0 - 2.0 * t;
101+
let a3 = 4.0;
102+
let a2 = 4.0 * theta;
50103
let a1 = theta * theta;
51104
let a0 = -norm_z_sq;
52105

53106
let cubic_poly_roots = roots::find_roots_cubic(a3, a2, a1, a0);
54-
let mut right_root = f64::NAN;
55-
let mut scaling = f64::NAN;
56-
57-
// Find right root
58-
cubic_poly_roots.as_ref().iter().for_each(|ri| {
59-
if *ri > 0. {
60-
let denom = 1. + 2. * (*ri - t);
61-
if ((norm_z_sq / (denom * denom)) - *ri).abs() < 1e-6 {
62-
right_root = *ri;
63-
scaling = denom;
107+
108+
let root_tol = 1e-6;
109+
let mut right_root: Option<f64> = None;
110+
111+
// Pick the first admissible real root
112+
for &ri in cubic_poly_roots.as_ref().iter() {
113+
let denom = 1.0 + 2.0 * (ri - t);
114+
115+
// We need a valid scaling and consistency with ||z_proj||^2 = ri
116+
if denom > 0.0 {
117+
let candidate_norm_sq = norm_z_sq / (denom * denom);
118+
if (candidate_norm_sq - ri).abs() <= root_tol {
119+
right_root = Some(ri);
120+
break;
64121
}
65122
}
66-
});
123+
}
124+
125+
let mut zsol =
126+
right_root.expect("EpigraphSquaredNorm::project: no admissible real root found");
67127

68-
// Refinement of root with Newton-Raphson
69-
let mut refinement_error = 1.;
128+
// Newton refinement
70129
let newton_max_iters: usize = 5;
71130
let newton_eps = 1e-14;
72-
let mut zsol = right_root;
73-
let mut iter = 0;
74-
while refinement_error > newton_eps && iter < newton_max_iters {
131+
132+
for _ in 0..newton_max_iters {
75133
let zsol_sq = zsol * zsol;
76134
let zsol_cb = zsol_sq * zsol;
135+
77136
let p_z = a3 * zsol_cb + a2 * zsol_sq + a1 * zsol + a0;
78-
let dp_z = 3. * a3 * zsol_sq + 2. * a2 * zsol + a1;
137+
if p_z.abs() <= newton_eps {
138+
break;
139+
}
140+
141+
let dp_z = 3.0 * a3 * zsol_sq + 2.0 * a2 * zsol + a1;
142+
assert!(
143+
dp_z.abs() > 1e-15,
144+
"EpigraphSquaredNorm::project: Newton derivative too small"
145+
);
146+
79147
zsol -= p_z / dp_z;
80-
refinement_error = p_z.abs();
81-
iter += 1;
82148
}
83-
right_root = zsol;
149+
150+
let right_root = zsol;
151+
let scaling = 1.0 + 2.0 * (right_root - t);
152+
153+
assert!(
154+
scaling.abs() > 1e-15,
155+
"EpigraphSquaredNorm::project: scaling factor too small"
156+
);
84157

85158
// Projection
86159
for xi in x.iter_mut().take(nx) {
@@ -89,7 +162,7 @@ impl Constraint for EpigraphSquaredNorm {
89162
x[nx] = right_root;
90163
}
91164

92-
/// This is a convex set, so this function returns `True`
165+
/// This is a convex set, so this function returns `true`.
93166
fn is_convex(&self) -> bool {
94167
true
95168
}

src/constraints/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ mod rectangle;
2222
mod simplex;
2323
mod soc;
2424
mod sphere2;
25-
mod squared_norm_epigraph;
2625
mod zero;
2726

2827
pub use affine_space::AffineSpace;
@@ -39,7 +38,6 @@ pub use rectangle::Rectangle;
3938
pub use simplex::Simplex;
4039
pub use soc::SecondOrderCone;
4140
pub use sphere2::Sphere2;
42-
pub use squared_norm_epigraph::EpiSqNorm;
4341
pub use zero::Zero;
4442

4543
/// A set which can be used as a constraint

0 commit comments

Comments
 (0)