ellalgo_rs/ell_stable.rs
1// mod lib;
2use crate::cutting_plane::{CutStatus, SearchSpace, SearchSpaceQ, UpdateByCutChoice};
3use crate::ell_calc::EllCalc;
4// #[macro_use]
5// extern crate ndarray;
6// use ndarray::prelude::*;
7use ndarray::Array1;
8use ndarray::Array2;
9
10/// The code defines a struct called [`EllStable`] that represents the stable version of an ellipsoid
11/// search space in the Ellipsoid method.
12///
13/// EllStable = {x | (x - xc)^T mq^-1 (x - xc) \le \kappa}
14///
15/// Properties:
16///
17/// * `no_defer_trick`: A boolean flag indicating whether the defer trick should be used. The defer
18/// trick is a technique used in the Ellipsoid method to improve efficiency by deferring the update of
19/// the ellipsoid until a certain condition is met.
20/// * `mq`: A matrix representing the shape of the ellipsoid. It is a 2-dimensional array of f64 values.
21/// * `xc`: The `xc` property represents the center of the ellipsoid search space. It is a 1-dimensional
22/// array of floating-point numbers.
23/// * `kappa`: A scalar value that determines the size of the ellipsoid. A larger value of kappa results
24/// in a larger ellipsoid.
25/// * `ndim`: The `ndim` property represents the number of dimensions of the ellipsoid search space.
26/// * `helper`: The `helper` property is an instance of the `EllCalc` struct, which is used to perform
27/// calculations related to the ellipsoid search space. It provides methods for calculating the distance
28/// constant (`dc`), the center constant (`cc`), and the quadratic constant (`q`) used in the ellip
29/// * `tsq`: The `tsq` property represents the squared Mahalanobis distance threshold of the ellipsoid.
30/// It is used to determine whether a point is inside or outside the ellipsoid.
31#[derive(Debug, Clone)]
32pub struct EllStable {
33 mq: Array2<f64>,
34 xc: Array1<f64>,
35 kappa: f64,
36 helper: EllCalc,
37 tsq: f64,
38}
39
40impl EllStable {
41 /// The function `new_with_matrix` constructs a new [`EllStable`] object with the given parameters.
42 ///
43 /// Arguments:
44 ///
45 /// * `kappa`: The `kappa` parameter is a floating-point number that represents the curvature of the
46 /// ellipse. It determines the shape of the ellipse, with higher values resulting in a more elongated
47 /// shape and lower values resulting in a more circular shape.
48 /// * `mq`: The `mq` parameter is of type `Array2<f64>`, which represents a 2-dimensional array of `f64`
49 /// (floating-point) values. It is used to store the matrix `mq` in the `EllStable` object.
50 /// * `xc`: The parameter `xc` represents the center of the ellipsoid in n-dimensional space. It is an
51 /// array of length `ndim`, where each element represents the coordinate of the center along a specific
52 /// dimension.
53 ///
54 /// Returns:
55 ///
56 /// an instance of the [`EllStable`] struct.
57 pub fn new_with_matrix(kappa: f64, mq: Array2<f64>, xc: Array1<f64>) -> EllStable {
58 let helper = EllCalc::new(xc.len());
59
60 EllStable {
61 kappa,
62 mq,
63 xc,
64 helper,
65 tsq: 0.0,
66 }
67 }
68
69 /// Creates a new [`EllStable`].
70 ///
71 /// The function `new` creates a new `EllStable` object with the given values.
72 ///
73 /// Arguments:
74 ///
75 /// * `val`: An array of f64 values representing the diagonal elements of a matrix.
76 /// * `xc`: `xc` is an `Array1<f64>` which represents the center of the ellipse. It contains the x and y
77 /// coordinates of the center point.
78 ///
79 /// Returns:
80 ///
81 /// The function `new` returns an instance of the [`EllStable`] struct.
82 pub fn new(val: Array1<f64>, xc: Array1<f64>) -> EllStable {
83 EllStable::new_with_matrix(1.0, Array2::from_diag(&val), xc)
84 }
85
86 /// The function `new_with_scalar` constructs a new [`EllStable`] object with a scalar value and a vector.
87 ///
88 /// Arguments:
89 ///
90 /// * `val`: The `val` parameter is a scalar value of type `f64`. It represents the value of the scalar
91 /// component of the `EllStable` object.
92 /// * `xc`: The parameter `xc` is an array of type `Array1<f64>`. It represents the center coordinates
93 /// of the ellipse.
94 ///
95 /// Returns:
96 ///
97 /// an instance of the [`EllStable`] struct.
98 pub fn new_with_scalar(val: f64, xc: Array1<f64>) -> EllStable {
99 EllStable::new_with_matrix(val, Array2::eye(xc.len()), xc)
100 }
101
102 /// Update ellipsoid core function using the cut
103 ///
104 /// $grad^T * (x - xc) + beta <= 0$
105 ///
106 /// The `update_core` function in Rust updates the ellipsoid core based on a given gradient and beta
107 /// value using a cut strategy.
108 ///
109 /// Reference:
110 /// Gill, Murray, and Wright, "Practical Optimization", p43. Author: Brian Borchers (borchers@nmt.edu)
111 ///
112 /// Arguments:
113 ///
114 /// * `grad`: A reference to an Array1<f64> representing the gradient vector.
115 /// * `beta`: The `beta` parameter is a value that is used in the inequality constraint of the ellipsoid
116 /// core function. It represents the threshold for the constraint, and the function checks if the dot
117 /// product of the gradient and the difference between `x` and `xc` plus `beta` is less than or
118 /// * `f_core`: The `f_core` parameter is a closure that takes two arguments: `beta` and `tsq`. It
119 /// returns a tuple containing a `CutStatus` and a tuple `(rho, sigma, delta)`. The `f_core` function is
120 /// used to determine the values of `rho`, `
121 ///
122 /// Returns:
123 ///
124 /// The `update_core` function returns a value of type `CutStatus`.
125 fn update_core<T, F>(&mut self, grad: &Array1<f64>, beta: &T, f_core: F) -> CutStatus
126 where
127 T: UpdateByCutChoice<Self, ArrayType = Array1<f64>>,
128 F: FnOnce(&T, f64) -> (CutStatus, (f64, f64, f64)),
129 {
130 // calculate inv(L)*grad: (n-1)*n/2 multiplications
131 let mut inv_ml_g = grad.clone(); // initial x0
132 let ndim = self.xc.len();
133 for i in 1..ndim {
134 for j in 0..i {
135 self.mq[[i, j]] = self.mq[[j, i]] * inv_ml_g[j];
136 // keep for rank-one update
137 inv_ml_g[i] -= self.mq[[i, j]];
138 }
139 }
140
141 // calculate inv(D)*inv(L)*grad: n
142 let mut inv_md_inv_ml_g = inv_ml_g.clone(); // initially
143 for i in 0..ndim {
144 inv_md_inv_ml_g[i] *= self.mq[[i, i]];
145 }
146
147 // calculate omega: n
148 let mut gg_t = inv_md_inv_ml_g.clone(); // initially
149 let mut omega = 0.0; // initially
150 for i in 0..ndim {
151 gg_t[i] *= inv_ml_g[i];
152 omega += gg_t[i];
153 }
154
155 self.tsq = self.kappa * omega;
156 let (status, (rho, sigma, delta)) = f_core(beta, self.tsq);
157
158 if status != CutStatus::Success {
159 return status;
160 }
161
162 // calculate mq*grad = inv(L')*inv(D)*inv(L)*grad : (n-1)*n/2
163 let mut g_t = inv_md_inv_ml_g.clone(); // initially
164 for i in (1..ndim).rev() {
165 // backward subsituition
166 for j in i..ndim {
167 g_t[i - 1] -= self.mq[[i - 1, j]] * g_t[j]; // ???
168 }
169 }
170
171 // calculate xc: n
172 self.xc -= &((rho / omega) * &g_t); // n
173
174 // Rank-one update: 3*n + (n-1)*n/2
175 //
176 // Reference:
177 // Gill, Murray, and Wright, "Practical Optimization", p43. Author: Brian Borchers (borchers@nmt.edu)
178 //
179 // let r = self.sigma / omega;
180 let mu = sigma / (1.0 - sigma);
181 let mut oldt = omega / mu; // initially
182 let m = ndim - 1;
183 for j in 0..m {
184 // p=sqrt(k)*vv[j];
185 // let p = inv_ml_g[j];
186 // let mup = mu * p;
187 let t = oldt + gg_t[j];
188 // self.mq[[j, j]] /= t; // update invD
189 let beta2 = inv_md_inv_ml_g[j] / t;
190 self.mq[[j, j]] *= oldt / t; // update invD
191 for l in (j + 1)..ndim {
192 // v(l) -= p * self.mq(j, l);
193 self.mq[[j, l]] += beta2 * self.mq[[l, j]];
194 }
195 oldt = t;
196 }
197
198 // let p = inv_ml_g(n1);
199 // let mup = mu * p;
200 let t = oldt + gg_t[m];
201 self.mq[[m, m]] *= oldt / t; // update invD
202 self.kappa *= delta;
203
204 // if self.no_defer_trick
205 // {
206 // self.mq *= self.kappa;
207 // self.kappa = 1.;
208 // }
209 status
210 }
211}
212
213impl SearchSpace for EllStable {
214 type ArrayType = Array1<f64>;
215
216 /// The function `xc` returns a copy of the `xc` array.
217 fn xc(&self) -> Self::ArrayType {
218 self.xc.clone()
219 }
220
221 /// The `tsq` function returns the value of the `tsq` field of the struct.
222 ///
223 /// Returns:
224 ///
225 /// The method `tsq` is returning a value of type `f64`.
226 fn tsq(&self) -> f64 {
227 self.tsq
228 }
229
230 /// The `update_bias_cut` function updates the decision variable based on the given cut.
231 ///
232 /// Arguments:
233 ///
234 /// * `cut`: A tuple containing two elements:
235 ///
236 /// Returns:
237 ///
238 /// The `update_bias_cut` function returns a value of type `CutStatus`.
239 fn update_bias_cut<T>(&mut self, cut: &(Self::ArrayType, T)) -> CutStatus
240 where
241 T: UpdateByCutChoice<Self, ArrayType = Self::ArrayType>,
242 {
243 let (grad, beta) = cut;
244 beta.update_bias_cut_by(self, grad)
245 }
246
247 /// The `update_central_cut` function updates the cut choices using the gradient and beta values.
248 ///
249 /// Arguments:
250 ///
251 /// * `cut`: The `cut` parameter is a tuple containing two elements. The first element is of type
252 /// `Self::ArrayType`, and the second element is of type `T`.
253 ///
254 /// Returns:
255 ///
256 /// The function `update_central_cut` returns a value of type `CutStatus`.
257 fn update_central_cut<T>(&mut self, cut: &(Self::ArrayType, T)) -> CutStatus
258 where
259 T: UpdateByCutChoice<Self, ArrayType = Self::ArrayType>,
260 {
261 let (grad, beta) = cut;
262 beta.update_central_cut_by(self, grad)
263 }
264
265 fn set_xc(&mut self, x: Self::ArrayType) {
266 self.xc = x;
267 }
268}
269
270impl SearchSpaceQ for EllStable {
271 type ArrayType = Array1<f64>;
272
273 /// The function `xc` returns a copy of the `xc` array.
274 fn xc(&self) -> Self::ArrayType {
275 self.xc.clone()
276 }
277
278 /// The `tsq` function returns the value of the `tsq` field of the struct.
279 ///
280 /// Returns:
281 ///
282 /// The method `tsq` is returning a value of type `f64`.
283 fn tsq(&self) -> f64 {
284 self.tsq
285 }
286
287 /// The `update_q` function updates the decision variable based on the given cut.
288 ///
289 /// Arguments:
290 ///
291 /// * `cut`: A tuple containing two elements:
292 ///
293 /// Returns:
294 ///
295 /// The `update_bias_cut` function returns a value of type `CutStatus`.
296 fn update_q<T>(&mut self, cut: &(Self::ArrayType, T)) -> CutStatus
297 where
298 T: UpdateByCutChoice<Self, ArrayType = Self::ArrayType>,
299 {
300 let (grad, beta) = cut;
301 beta.update_q_by(self, grad)
302 }
303}
304
305impl UpdateByCutChoice<EllStable> for f64 {
306 type ArrayType = Array1<f64>;
307
308 fn update_bias_cut_by(&self, ellip: &mut EllStable, grad: &Self::ArrayType) -> CutStatus {
309 let beta = self;
310 let helper = ellip.helper.clone();
311 ellip.update_core(grad, beta, |beta, tsq| helper.calc_bias_cut(*beta, tsq))
312 }
313
314 fn update_central_cut_by(&self, ellip: &mut EllStable, grad: &Self::ArrayType) -> CutStatus {
315 let beta = self;
316 let helper = ellip.helper.clone();
317 ellip.update_core(grad, beta, |_beta, tsq| helper.calc_central_cut(tsq))
318 }
319
320 fn update_q_by(&self, ellip: &mut EllStable, grad: &Self::ArrayType) -> CutStatus {
321 let beta = self;
322 let helper = ellip.helper.clone();
323 ellip.update_core(grad, beta, |beta, tsq| helper.calc_bias_cut_q(*beta, tsq))
324 }
325}
326
327impl UpdateByCutChoice<EllStable> for (f64, Option<f64>) {
328 type ArrayType = Array1<f64>;
329
330 fn update_bias_cut_by(&self, ellip: &mut EllStable, grad: &Self::ArrayType) -> CutStatus {
331 let beta = self;
332 let helper = ellip.helper.clone();
333 ellip.update_core(grad, beta, |beta, tsq| {
334 helper.calc_single_or_parallel_bias_cut(beta, tsq)
335 })
336 }
337
338 fn update_central_cut_by(&self, ellip: &mut EllStable, grad: &Self::ArrayType) -> CutStatus {
339 let beta = self;
340 let helper = ellip.helper.clone();
341 ellip.update_core(grad, beta, |beta, tsq| {
342 helper.calc_single_or_parallel_central_cut(beta, tsq)
343 })
344 }
345
346 fn update_q_by(&self, ellip: &mut EllStable, grad: &Self::ArrayType) -> CutStatus {
347 let beta = self;
348 let helper = ellip.helper.clone();
349 ellip.update_core(grad, beta, |beta, tsq| {
350 helper.calc_single_or_parallel_q(beta, tsq)
351 })
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use approx_eq::assert_approx_eq;
359
360 #[test]
361 fn test_construct() {
362 let ellip = EllStable::new_with_scalar(0.01, Array1::zeros(4));
363 assert_approx_eq!(ellip.kappa, 0.01);
364 assert_eq!(ellip.xc, Array1::zeros(4));
365 assert_approx_eq!(ellip.tsq, 0.0);
366 }
367
368 #[test]
369 fn test_update_central_cut() {
370 let mut ellip = EllStable::new_with_scalar(0.01, Array1::zeros(4));
371 let cut = (0.5 * Array1::ones(4), 0.0);
372 let status = ellip.update_central_cut(&cut);
373 assert_eq!(status, CutStatus::Success);
374 assert_eq!(ellip.xc, -0.01 * Array1::ones(4));
375 assert_approx_eq!(ellip.kappa, 0.16 / 15.0);
376 assert_approx_eq!(ellip.tsq, 0.01);
377 }
378
379 #[test]
380 fn test_update_bias_cut() {
381 let mut ellip = EllStable::new_with_scalar(0.01, Array1::zeros(4));
382 let cut = (0.5 * Array1::ones(4), 0.05);
383 let status = ellip.update_bias_cut(&cut);
384 assert_eq!(status, CutStatus::Success);
385 assert_approx_eq!(ellip.xc[0], -0.03);
386 assert_approx_eq!(ellip.kappa, 0.008);
387 assert_approx_eq!(ellip.tsq, 0.01);
388 }
389
390 #[test]
391 fn test_update_parallel_central_cut() {
392 let mut ellip = EllStable::new_with_scalar(0.01, Array1::zeros(4));
393 let cut = (0.5 * Array1::ones(4), (0.0, Some(0.05)));
394 let status = ellip.update_central_cut(&cut);
395 assert_eq!(status, CutStatus::Success);
396 assert_eq!(ellip.xc, -0.01 * Array1::ones(4));
397 assert_approx_eq!(ellip.kappa, 0.012);
398 assert_approx_eq!(ellip.tsq, 0.01);
399 }
400
401 #[test]
402 fn test_update_parallel() {
403 let mut ellip = EllStable::new_with_scalar(0.01, Array1::zeros(4));
404 let cut = (0.5 * Array1::ones(4), (0.01, Some(0.04)));
405 let status = ellip.update_bias_cut(&cut);
406 assert_eq!(status, CutStatus::Success);
407 assert_approx_eq!(ellip.xc[0], -0.0116);
408 assert_approx_eq!(ellip.kappa, 0.01232);
409 assert_approx_eq!(ellip.tsq, 0.01);
410 }
411
412 #[test]
413 fn test_update_parallel_no_effect() {
414 let mut ellip = EllStable::new_with_scalar(0.01, Array1::zeros(4));
415 let cut = (0.5 * Array1::ones(4), (-0.04, Some(0.0625)));
416 let status = ellip.update_bias_cut(&cut);
417 assert_eq!(status, CutStatus::Success);
418 assert_eq!(ellip.xc, Array1::zeros(4));
419 assert_approx_eq!(ellip.kappa, 0.01);
420 }
421
422 #[test]
423 fn test_update_q_no_effect() {
424 let mut ellip = EllStable::new_with_scalar(0.01, Array1::zeros(4));
425 let cut = (0.5 * Array1::ones(4), (-0.04, Some(0.0625)));
426 let status = ellip.update_q(&cut);
427 assert_eq!(status, CutStatus::NoEffect);
428 assert_eq!(ellip.xc, Array1::zeros(4));
429 assert_approx_eq!(ellip.kappa, 0.01);
430 }
431
432 #[test]
433 fn test_update_q() {
434 let mut ellip = EllStable::new_with_scalar(0.01, Array1::zeros(4));
435 let cut = (0.5 * Array1::ones(4), (0.01, Some(0.04)));
436 let status = ellip.update_q(&cut);
437 assert_eq!(status, CutStatus::Success);
438 assert_approx_eq!(ellip.xc[0], -0.0116);
439 assert_approx_eq!(ellip.kappa, 0.01232);
440 assert_approx_eq!(ellip.tsq, 0.01);
441 }
442}