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}