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}