@@ -3,84 +3,157 @@ use crate::matrix_operations;
33use 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`.
813pub struct EpigraphSquaredNorm { }
914
1015impl 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
1925impl 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 }
0 commit comments