numdiff/central_difference/hessian/
vector_valued.rs

1use crate::constants::CBRT_EPS;
2use linalg_traits::Vector;
3
4/// Hessian of a multivariate, vector-valued function using the central difference approximation.
5///
6/// # Arguments
7///
8/// * `f` - Multivariate, vector-valued function, $\mathbf{}:\mathbb{R}^{n}\to\mathbb{R}^{m}$.
9/// * `x0` - Evaluation point, $\mathbf{x}_{0}\in\mathbb{R}^{n}$.
10/// * `h` - Relative step size, $h\in\mathbb{R}$. Defaults to [`CBRT_EPS`].
11///
12/// # Returns
13///
14/// Hessian of $\mathbf{f}$ with respect to $\mathbf{x}$, evaluated at $\mathbf{x}=\mathbf{x}_{0}$.
15///
16/// $$\mathbf{H}(\mathbf{f}(\mathbf{x}_{0}))\in\mathbb{R}^{m\times n\times n}$$
17///
18/// The returned result is stored as a length-$m$ [`Vec`] of $n\times n$ matrices.
19///
20/// # Note
21///
22/// This function performs $2n(n+1)$ evaluations of $f(x)$.
23///
24/// # Example
25///
26/// Approximate the Hessian of
27///
28/// $$
29/// \mathbf{f}(\mathbf{x})=
30/// \begin{bmatrix}
31///     f_{0}(\mathbf{x}) \\\\
32///     f_{1}(\mathbf{x})
33/// \end{bmatrix}=
34/// \begin{bmatrix}
35///     x_{0}^{5}x_{1}+x_{0}\sin^{3}{x_{1}} \\\\
36///     x_{0}^{3}+x_{1}^{4}-3x_{0}^{2}x_{1}^{2}
37/// \end{bmatrix}
38/// $$
39///
40/// at $\mathbf{x}=\mathbf{x}_{0}=(5,8)^{T}$, and compare the result to the true result of
41///
42/// $$
43/// \mathbf{H}\left(\mathbf{x}_{0}\right)=\left[\mathbf{H}\left(f\_{0}(\mathbf{x}\_{0})\right),
44/// \\;\mathbf{H}\left(f\_{1}(\mathbf{x}\_{0})\right)\right]
45/// $$
46///
47/// where
48///
49/// $$
50/// \begin{aligned}
51///     \mathbf{H}\left(f\_{0}(\mathbf{x}\_{0})\right)&=
52///     \begin{bmatrix}
53///         20x_{0}^{3}x_{1} & 5x_{0}^{4}+3\sin^{2}{x_{1}}\cos{x_{1}} \\\\
54///         5x_{0}^{4}+3\sin^{2}{x_{1}}\cos{x_{1}} & 6x_{0}\sin{x_{1}}\cos^{2}{x_{1}}-3x_{0}\sin^{3}{x_{1}}
55///     \end{bmatrix}
56///     \bigg\rvert_{\mathbf{x}=(5,8)^{T}} \\\\
57///     &=
58///     \begin{bmatrix}
59///         20(5)^{3}(8) & 5(5)^{4}+3\sin^{2}{(8)}\cos{(8)} \\\\
60///         5(5)^{4}+3\sin^{2}{(8)}\cos{(8)} & 6(5)\sin{(8)}\cos^{2}{(8)}-3(5)\sin^{3}{(8)}
61///     \end{bmatrix} \\\\
62///     &=
63///     \begin{bmatrix}
64///         20000 & 3125+3\sin^{2}{(8)}\cos{(8)} \\\\
65///         3125+3\sin^{2}{(8)}\cos{(8)} & 30\sin{(8)}\cos^{2}{(8)}-15\sin^{3}{(8)}
66///     \end{bmatrix} \\\\
67///     \mathbf{H}\left(f\_{1}(\mathbf{x}\_{0})\right)&=
68///     \begin{bmatrix}
69///         6x_{0}-6x_{1}^{2} & -12x_{0}x_{1} \\\\
70///         -12x_{0}x_{1} & 12x_{1}^{2}-6x_{0}^{2}
71///     \end{bmatrix}
72///     \bigg\rvert_{\mathbf{x}=(5,8)^{T}} \\\\
73///     &=
74///     \begin{bmatrix}
75///         6(5)-6(8)^{2} & -12(5)(8) \\\\
76///         -12(5)(8) & 12(8)^{2}-6(5)^{2}
77///     \end{bmatrix} \\\\
78///     &=
79///     \begin{bmatrix}
80///         -354 & -480 \\\\
81///         -480 & 618
82///     \end{bmatrix}
83/// \end{aligned}
84/// $$
85///
86/// #### Using standard vectors
87///
88/// ```
89/// use linalg_traits::{Mat, Matrix};
90/// use numtest::*;
91///
92/// use numdiff::central_difference::vhessian;
93///
94/// // Define the function, f(x).
95/// let f = |x: &Vec<f64>| vec![
96///     x[0].powi(5) * x[1] + x[0] * x[1].sin().powi(3),
97///     x[0].powi(3) + x[1].powi(4) - 3.0 * x[0].powi(2) * x[1].powi(2)
98/// ];
99///
100/// // Define the evaluation point.
101/// let x0 = vec![5.0, 8.0];
102///
103/// // Approximate the Hessian of f(x) at the evaluation point.
104/// let hess: Vec<Mat<f64>> = vhessian(&f, &x0, None);
105///
106/// // True Hessian of f(x) at the evaluation point.
107/// let hess_f0_true: Mat<f64> = Mat::from_row_slice(
108///     2,
109///     2,
110///     &[
111///         20000.0,
112///         3125.0 + 3.0 * 8.0_f64.sin().powi(2) * 8.0_f64.cos(),
113///         3125.0 + 3.0 * 8.0_f64.sin().powi(2) * 8.0_f64.cos(),
114///         30.0 * 8.0_f64.sin() * 8.0_f64.cos().powi(2) - 15.0 * 8.0_f64.sin().powi(3)
115///     ]
116/// );
117/// let hess_f1_true: Mat<f64> = Mat::from_row_slice(2, 2, &[-354.0, -480.0, -480.0, 618.0]);
118/// let hess_true: Vec<Mat<f64>> = vec![hess_f0_true, hess_f1_true];
119///
120/// // Check the accuracy of the Hessian approximation.
121/// assert_arrays_equal_to_decimal!(hess[0], hess_true[0], 3);
122/// assert_arrays_equal_to_decimal!(hess[1], hess_true[1], 3);
123/// ```
124///
125/// #### Using other vector types
126///
127/// We can also use other types of vectors, such as `nalgebra::SVector`, `nalgebra::DVector`,
128/// `ndarray::Array1`, `faer::Mat`, or any other type of vector that implements the
129/// `linalg_traits::Vector` trait.
130///
131/// ```
132/// use faer::Mat as FMat;
133/// use linalg_traits::{Mat, Matrix, Vector};
134/// use nalgebra::{dvector, DMatrix, DVector, SMatrix, SVector};
135/// use ndarray::{array, Array1, Array2};
136/// use numtest::*;
137///
138/// use numdiff::central_difference::vhessian;
139///
140/// let hess_f0_true: Mat<f64> = Mat::from_row_slice(
141///     2,
142///     2,
143///     &[
144///         20000.0,
145///         3125.0 + 3.0 * 8.0_f64.sin().powi(2) * 8.0_f64.cos(),
146///         3125.0 + 3.0 * 8.0_f64.sin().powi(2) * 8.0_f64.cos(),
147///         30.0 * 8.0_f64.sin() * 8.0_f64.cos().powi(2) - 15.0 * 8.0_f64.sin().powi(3)
148///     ]
149/// );
150/// let hess_f1_true: Mat<f64> = Mat::from_row_slice(2, 2, &[-354.0, -480.0, -480.0, 618.0]);
151/// let hess_true: Vec<Mat<f64>> = vec![hess_f0_true, hess_f1_true];
152///
153/// // nalgebra::DVector
154/// let f_dvector = |x: &DVector<f64>| dvector![
155///     x[0].powi(5) * x[1] + x[0] * x[1].sin().powi(3),
156///     x[0].powi(3) + x[1].powi(4) - 3.0 * x[0].powi(2) * x[1].powi(2)
157/// ];
158/// let x0_dvector: DVector<f64> = dvector![5.0, 8.0];
159/// let hess_dvector: Vec<DMatrix<f64>> = vhessian(&f_dvector, &x0_dvector, None);
160/// assert_arrays_equal_to_decimal!(hess_dvector[0], hess_true[0], 3);
161/// assert_arrays_equal_to_decimal!(hess_dvector[1], hess_true[1], 3);
162///
163/// // nalgebra::SVector
164/// let f_svector = |x: &SVector<f64, 2>| SVector::<f64, 2>::from_row_slice(&[
165///     x[0].powi(5) * x[1] + x[0] * x[1].sin().powi(3),
166///     x[0].powi(3) + x[1].powi(4) - 3.0 * x[0].powi(2) * x[1].powi(2)
167/// ]);
168/// let x0_svector: SVector<f64, 2> = SVector::from_row_slice(&[5.0, 8.0]);
169/// let hess_svector: Vec<SMatrix<f64, 2, 2>> = vhessian(&f_svector, &x0_svector, None);
170/// assert_arrays_equal_to_decimal!(hess_svector[0], hess_true[0], 3);
171/// assert_arrays_equal_to_decimal!(hess_svector[1], hess_true[1], 3);
172///
173/// // ndarray::Array1
174/// let f_array1 = |x: &Array1<f64>| array![
175///     x[0].powi(5) * x[1] + x[0] * x[1].sin().powi(3),
176///     x[0].powi(3) + x[1].powi(4) - 3.0 * x[0].powi(2) * x[1].powi(2)
177/// ];
178/// let x0_array1: Array1<f64> = array![5.0, 8.0];
179/// let hess_array1: Vec<Array2<f64>> = vhessian(&f_array1, &x0_array1, None);
180/// assert_arrays_equal_to_decimal!(hess_array1[0], hess_true[0], 3);
181/// assert_arrays_equal_to_decimal!(hess_array1[1], hess_true[1], 3);
182///
183/// // faer::Mat
184/// let f_mat = |x: &FMat<f64>| FMat::from_slice(
185///     &[
186///         x[(0, 0)].powi(5) * x[(1, 0)] + x[(0, 0)] * x[(1, 0)].sin().powi(3),
187///         x[(0, 0)].powi(3) + x[(1, 0)].powi(4) - 3.0 * x[(0, 0)].powi(2) * x[(1, 0)].powi(2)
188///     ]
189/// );
190/// let x0_mat: FMat<f64> = FMat::from_slice(&[5.0, 8.0]);
191/// let hess_mat: Vec<FMat<f64>> = vhessian(&f_mat, &x0_mat, None);
192/// assert_arrays_equal_to_decimal!(hess_mat[0].as_row_slice(), hess_true[0], 3);
193/// assert_arrays_equal_to_decimal!(hess_mat[1].as_row_slice(), hess_true[1], 3);
194/// ```
195///
196/// #### Modifying the relative step size
197///
198/// We can also modify the relative step size. Choosing a coarser relative step size, we get a worse
199/// approximation.
200///
201/// ```
202/// use linalg_traits::{Mat, Matrix};
203/// use numtest::*;
204///
205/// use numdiff::central_difference::vhessian;
206///
207/// let f = |x: &Vec<f64>| vec![
208///     x[0].powi(5) * x[1] + x[0] * x[1].sin().powi(3),
209///     x[0].powi(3) + x[1].powi(4) - 3.0 * x[0].powi(2) * x[1].powi(2)
210/// ];
211/// let x0 = vec![5.0, 8.0];
212///
213/// let hess: Vec<Mat<f64>> = vhessian(&f, &x0, Some(0.001));
214/// let hess_f0_true: Mat<f64> = Mat::from_row_slice(
215///     2,
216///     2,
217///     &[
218///         20000.0,
219///         3125.0 + 3.0 * 8.0_f64.sin().powi(2) * 8.0_f64.cos(),
220///         3125.0 + 3.0 * 8.0_f64.sin().powi(2) * 8.0_f64.cos(),
221///         30.0 * 8.0_f64.sin() * 8.0_f64.cos().powi(2) - 15.0 * 8.0_f64.sin().powi(3)
222///     ]
223/// );
224/// let hess_f1_true: Mat<f64> = Mat::from_row_slice(2, 2, &[-354.0, -480.0, -480.0, 618.0]);
225/// let hess_true: Vec<Mat<f64>> = vec![hess_f0_true, hess_f1_true];
226///
227/// assert_arrays_equal_to_decimal!(hess[0], hess_true[0], 1);
228/// assert_arrays_equal_to_decimal!(hess[1], hess_true[1], 3);
229/// ```
230pub fn vhessian<V, U>(f: &impl Fn(&V) -> U, x0: &V, h: Option<f64>) -> Vec<V::MatrixNxN>
231where
232    V: Vector<f64>,
233    U: Vector<f64>,
234{
235    // Copy the evaluation point so that we may modify it.
236    let mut x0 = x0.clone();
237
238    // Default the relative step size to h = ε¹ᐟ³ if not specified.
239    let h = h.unwrap_or(*CBRT_EPS);
240
241    // Determine the dimension of x.
242    let n = x0.len();
243
244    // Variable to store the number of rows in the Hessian.
245    let mut m = 0;
246
247    // Variable to store the Hessian.
248    let mut hess: Vec<V::MatrixNxN> = vec![x0.new_matrix_n_by_n(); m];
249
250    // Variables to store the original values of the evaluation points in the jth and kth
251    // directions.
252    let mut x0j: f64;
253    let mut x0k: f64;
254
255    // Variable to store the (j,k)th and (k,j)th elements of each Hessian.
256    let mut hess_jk: U;
257
258    // Vector to store absolute step sizes.
259    let mut a = vec![0.0; n];
260
261    // Populate vector of absolute step sizes.
262    for (k, ak) in a.iter_mut().enumerate().take(n) {
263        *ak = h * (1.0 + x0.vget(k).abs());
264    }
265
266    // Variables to store evaluations of f(x) at various perturbed points.
267    let mut b: U;
268    let mut c: U;
269    let mut d: U;
270    let mut e: U;
271
272    // Track whether the vectors of Hessians has been initialized.
273    let mut not_initialized = true;
274
275    // Evaluate the Hessian, iterating over the upper triangular elements.
276    for k in 0..n {
277        for j in k..n {
278            // Original value of the evaluation point in the jth and kth directions.
279            x0j = x0.vget(j);
280            x0k = x0.vget(k);
281
282            // Step forward in the jth and kth directions.
283            x0.vset(j, x0.vget(j) + a[j]);
284            x0.vset(k, x0.vget(k) + a[k]);
285            b = f(&x0);
286            x0.vset(j, x0j);
287            x0.vset(k, x0k);
288
289            // Step forward in the jth direction and backward in the kth direction.
290            x0.vset(j, x0.vget(j) + a[j]);
291            x0.vset(k, x0.vget(k) - a[k]);
292            c = f(&x0);
293            x0.vset(j, x0j);
294            x0.vset(k, x0k);
295
296            // Step backward in the jth direction and forward in the kth direction.
297            x0.vset(j, x0.vget(j) - a[j]);
298            x0.vset(k, x0.vget(k) + a[k]);
299            d = f(&x0);
300            x0.vset(j, x0j);
301            x0.vset(k, x0k);
302
303            // Step backward in the jth and kth directions.
304            x0.vset(j, x0.vget(j) - a[j]);
305            x0.vset(k, x0.vget(k) - a[k]);
306            e = f(&x0);
307            x0.vset(j, x0j);
308            x0.vset(k, x0k);
309
310            // Evaluate the (j,k)th and (k,j)th elements of each Hessian.
311            hess_jk = b.sub(&c).sub(&d).add(&e).div(4.0 * a[j] * a[k]);
312
313            // In the very first iteration, determine the number of rows in each Hessian.
314            if not_initialized {
315                m = hess_jk.len();
316                hess = vec![x0.new_matrix_n_by_n(); m];
317                not_initialized = false;
318            }
319
320            // Store the (j,k)th and (k,j)th elements of each Hessian.
321            for (i, hess_i) in hess.iter_mut().enumerate().take(m) {
322                hess_i[(j, k)] = hess_jk.vget(i);
323                hess_i[(k, j)] = hess_jk.vget(i);
324            }
325        }
326    }
327
328    // Return the result.
329    hess
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use linalg_traits::{Mat, Matrix};
336    use nalgebra::{DMatrix, DVector, SMatrix, SVector};
337    use ndarray::{Array1, Array2};
338    use numtest::*;
339
340    #[test]
341    fn test_vhessian_1() {
342        let f = |x: &Vec<f64>| vec![x[0].powi(3)];
343        let x0 = vec![2.0];
344        let hess = |x: &Vec<f64>| vec![Mat::from_row_slice(1, 1, &[6.0 * x[0]])];
345        assert_arrays_equal_to_decimal!(vhessian(&f, &x0, None)[0], hess(&x0)[0], 7);
346    }
347
348    #[test]
349    fn test_vhessian_2() {
350        let f =
351            |x: &SVector<f64, 2>| SVector::<f64, 1>::from_row_slice(&[x[0].powi(2) + x[1].powi(3)]);
352        let x0 = SVector::from_row_slice(&[1.0, 2.0]);
353        let hess = |x: &SVector<f64, 2>| {
354            vec![SMatrix::<f64, 2, 2>::from_row_slice(&[
355                2.0,
356                0.0,
357                0.0,
358                6.0 * x[1],
359            ])]
360        };
361        assert_arrays_equal_to_decimal!(vhessian(&f, &x0, None)[0], hess(&x0)[0], 5);
362    }
363
364    #[test]
365    fn test_vhessian_3() {
366        let f = |x: &Array1<f64>| {
367            Array1::<f64>::from_slice(&[x[0].powi(5) * x[1] + x[0] * x[1].sin().powi(3)])
368        };
369        let x0 = Array1::from_slice(&[1.0, 2.0]);
370        let hess = |x: &Array1<f64>| {
371            vec![Array2::<f64>::from_row_slice(
372                2,
373                2,
374                &[
375                    20.0 * x[0].powi(3) * x[1],
376                    5.0 * x[0].powi(4) + 3.0 * x[1].sin().powi(2) * x[1].cos(),
377                    5.0 * x[0].powi(4) + 3.0 * x[1].sin().powi(2) * x[1].cos(),
378                    6.0 * x[0] * x[1].sin() * x[1].cos().powi(2) - 3.0 * x[0] * x[1].sin().powi(3),
379                ],
380            )]
381        };
382        assert_arrays_equal_to_decimal!(vhessian(&f, &x0, None)[0], hess(&x0)[0], 5);
383    }
384
385    #[test]
386    fn test_vhessian_4() {
387        let f = |x: &DVector<f64>| {
388            DVector::<f64>::from_slice(&[
389                x[0].powi(5) * x[1] + x[0] * x[1].sin().powi(3),
390                x[0].powi(3) + x[1].powi(4) - 3.0 * x[0].powi(2) * x[1].powi(2),
391            ])
392        };
393        let x0 = DVector::<f64>::from_slice(&[1.0, 2.0]);
394        let hess = |x: &DVector<f64>| {
395            vec![
396                DMatrix::<f64>::from_row_slice(
397                    2,
398                    2,
399                    &[
400                        20.0 * x[0].powi(3) * x[1],
401                        5.0 * x[0].powi(4) + 3.0 * x[1].sin().powi(2) * x[1].cos(),
402                        5.0 * x[0].powi(4) + 3.0 * x[1].sin().powi(2) * x[1].cos(),
403                        6.0 * x[0] * x[1].sin() * x[1].cos().powi(2)
404                            - 3.0 * x[0] * x[1].sin().powi(3),
405                    ],
406                ),
407                DMatrix::<f64>::from_row_slice(
408                    2,
409                    2,
410                    &[
411                        6.0 * x[0].powi(2) - 6.0 * x[1].powi(2),
412                        -12.0 * x[0] * x[1],
413                        -12.0 * x[0] * x[1],
414                        12.0 * x[1].powi(2) - 6.0 * x[0].powi(2),
415                    ],
416                ),
417            ]
418        };
419        assert_arrays_equal_to_decimal!(vhessian(&f, &x0, None)[0], hess(&x0)[0], 5);
420        assert_arrays_equal_to_decimal!(vhessian(&f, &x0, None)[1], hess(&x0)[1], 5);
421    }
422}