numdiff/automatic_differentiation/
gradient.rs

1/// Get a function that returns the gradient of the provided multivariate, scalar-valued function.
2///
3/// The gradient is computed using forward-mode automatic differentiation.
4///
5/// # Arguments
6///
7/// * `f` - Multivariate, scalar-valued function, $f:\mathbb{R}^{n}\to\mathbb{R}$.
8/// * `func_name` - Name of the function that will return the gradient of $f(\mathbf{x})$ at any
9///   point $\mathbf{x}\in\mathbb{R}^{n}$.
10///
11/// # Warning
12///
13/// `f` cannot be defined as closure. It must be defined as a function.
14///
15/// # Note
16///
17/// The function produced by this macro will perform $n$ evaluations of $f(\mathbf{x})$ to evaluate
18/// its gradient.
19///
20/// # Examples
21///
22/// ## Basic Example
23///
24/// Compute the gradient of
25///
26/// $$f(\mathbf{x})=x_{0}^{5}+\sin^{3}{x_{1}}$$
27///
28/// at $\mathbf{x}=(5,8)^{T}$, and compare the result to the true result of
29///
30/// $$
31/// \nabla f\left((5,8)^{T}\right)=
32/// \begin{bmatrix}
33///     3125 \\\\
34///     3\sin^{2}{(8)}\cos{(8)}
35/// \end{bmatrix}
36/// $$
37///
38/// #### Using standard vectors
39///
40/// ```
41/// use linalg_traits::{Scalar, Vector};
42/// use numtest::*;
43///
44/// use numdiff::{get_gradient, Dual, DualVector};
45///
46/// // Define the function, f(x).
47/// fn f<S: Scalar, V: Vector<S>>(x: &V, _p: &[f64]) -> S {
48///     x.vget(0).powi(5) + x.vget(1).sin().powi(3)
49/// }
50///
51/// // Define the evaluation point.
52/// let x0 = vec![5.0, 8.0];
53///
54/// // Parameter vector (empty for this example).
55/// let p = [];
56///
57/// // Autogenerate the function "g" that can be used to compute the gradient of f(x) at any point
58/// // x.
59/// get_gradient!(f, g);
60///
61/// // Function defining the true gradient of f(x).
62/// let g_true = |x: &Vec<f64>| vec![5.0 * x[0].powi(4), 3.0 * x[1].sin().powi(2) * x[1].cos()];
63///
64/// // Evaluate the gradient using "g".
65/// let g_eval: Vec<f64> = g(&x0, &p);
66///
67/// // Verify that the gradient function obtained using get_gradient! computes the gradient
68/// // correctly.
69/// assert_arrays_equal_to_decimal!(g(&x0, &p), g_true(&x0), 15);
70/// ```
71///
72/// #### Using other vector types
73///
74/// The function produced by `get_gradient!` can accept _any_ type for `x0`, as long as it
75/// implements the `linalg_traits::Vector` trait.
76///
77/// ```
78/// use faer::Mat;
79/// use linalg_traits::{Scalar, Vector};
80/// use nalgebra::{dvector, DVector, SVector};
81/// use ndarray::{array, Array1};
82///
83/// use numdiff::{get_gradient, Dual, DualVector};
84///
85/// // Define the function, f(x).
86/// fn f<S: Scalar, V: Vector<S>>(x: &V, _p: &[f64]) -> S {
87///     x.vget(0).powi(5) + x.vget(1).sin().powi(3)
88/// }
89///
90/// // Parameter vector (empty for this example).
91/// let p = [];
92///
93/// // Autogenerate the function "g" that can be used to compute the gradient of f(x) at any point
94/// // x.
95/// get_gradient!(f, g);
96///
97/// // nalgebra::DVector
98/// let x0: DVector<f64> = dvector![5.0, 8.0];
99/// let g_eval: DVector<f64> = g(&x0, &p);
100///
101/// // nalgebra::SVector
102/// let x0: SVector<f64, 2> = SVector::from_slice(&[5.0, 8.0]);
103/// let g_eval: SVector<f64, 2> = g(&x0, &p);
104///
105/// // ndarray::Array1
106/// let x0: Array1<f64> = array![5.0, 8.0];
107/// let g_eval: Array1<f64> = g(&x0, &p);
108///
109/// // faer::Mat
110/// let x0: Mat<f64> = Mat::from_slice(&[5.0, 8.0]);
111/// let g_eval: Mat<f64> = g(&x0, &p);
112/// ```
113///
114/// ## Example Passing Runtime Parameters
115///
116/// Compute the gradient of a parameterized function
117///
118/// $$f(\mathbf{x})=ax_{0}^{2}+bx_{1}^{2}+cx_{0}x_{1}+d$$
119///
120/// where $a$, $b$, $c$, and $d$ are runtime parameters. Compare the result against the true
121/// gradient of
122///
123/// $$\nabla f=\begin{bmatrix}2ax_{0}+cx_{1}\\\\2bx_{1}+cx_{0}\end{bmatrix}$$
124///
125/// ```
126/// use linalg_traits::{Scalar, Vector};
127/// use numtest::*;
128///
129/// use numdiff::{get_gradient, Dual, DualVector};
130///
131/// // Define the function, f(x).
132/// fn f<S: Scalar, V: Vector<S>>(x: &V, p: &[f64]) -> S {
133///     let a = S::new(p[0]);
134///     let b = S::new(p[1]);
135///     let c = S::new(p[2]);
136///     let d = S::new(p[3]);
137///     a * x.vget(0).powi(2) + b * x.vget(1).powi(2) + c * x.vget(0) * x.vget(1) + d
138/// }
139///
140/// // Parameter vector.
141/// let a = 2.0;
142/// let b = 1.5;
143/// let c = 0.8;
144/// let d = -3.0;
145/// let p = [a, b, c, d];
146///
147/// // Evaluation point.
148/// let x0 = vec![1.0, -2.0];
149///
150/// // Autogenerate the gradient function.
151/// get_gradient!(f, g);
152///
153/// // True gradient function.
154/// let g_true = |x: &Vec<f64>| vec![2.0 * a * x[0] + c * x[1], 2.0 * b * x[1] + c * x[0]];
155///
156/// // Compute the gradient using both the automatically generated gradient function and the true
157/// // gradient function, and compare the results.
158/// let g_eval: Vec<f64> = g(&x0, &p);
159/// let g_eval_true: Vec<f64> = g_true(&x0);
160/// assert_arrays_equal_to_decimal!(g_eval, g_eval_true, 15);
161/// ```
162#[macro_export]
163macro_rules! get_gradient {
164    ($f:ident, $func_name:ident) => {
165        /// Gradient of a multivariate, scalar-valued function `f: ℝⁿ → ℝ`.
166        ///
167        /// This function is generated for a specific function `f` using the
168        /// `numdiff::get_gradient!` macro.
169        ///
170        /// # Arguments
171        ///
172        /// * `x0` - Evaluation point, `x₀ ∈ ℝⁿ`.
173        /// * `p` - Parameter vector. This is a vector of additional runtime parameters that the
174        ///   function may depend on but is not differentiated with respect to.
175        ///
176        /// # Returns
177        ///
178        /// Gradient of `f` with respect to `x`, evaluated at `x = x₀`.
179        ///
180        /// `∇f(x₀) ∈ ℝⁿ`
181        fn $func_name<S, V>(x0: &V, p: &[f64]) -> V::Vectorf64
182        where
183            S: Scalar,
184            V: Vector<S>,
185        {
186            // Preallocate the vector to store the gradient.
187            let mut g: V::Vectorf64 = x0.new_vector_f64();
188
189            // Promote the evaluation point to a vector of dual numbers.
190            let mut x0_dual = x0.clone().to_dual_vector();
191
192            // Variable to store the original value of the evaluation point in the kth dual
193            // direction.
194            let mut x0k: Dual;
195
196            // Evaluate the gradient.
197            for k in 0..x0_dual.len() {
198                // Original value of the evaluation point in the kth dual direction.
199                x0k = x0_dual.vget(k);
200
201                // Take a unit step forward in the kth dual direction.
202                x0_dual.vset(k, Dual::new(x0k.get_real(), 1.0));
203
204                // Partial derivative of f with respect to xₖ.
205                g.vset(k, $f(&x0_dual, p).get_dual());
206
207                // Reset the evaluation point.
208                x0_dual.vset(k, x0k);
209            }
210
211            // Return the result.
212            g
213        }
214    };
215}
216
217#[cfg(test)]
218mod tests {
219    use crate::automatic_differentiation::dual::Dual;
220    use crate::automatic_differentiation::dual_vector::DualVector;
221    use linalg_traits::{Scalar, Vector};
222    use nalgebra::DVector;
223    use ndarray::{Array1, array};
224    use numtest::*;
225
226    #[test]
227    fn test_gradient_1() {
228        // Function to find the gradient of.
229        fn f<S: Scalar, V: Vector<S>>(x: &V, _p: &[f64]) -> S {
230            x.vget(0).powi(2)
231        }
232
233        // Evaluation point.
234        let x0 = vec![2.0];
235
236        // Parameter vector (empty for this test).
237        let p = [];
238
239        // True gradient function.
240        let g = |x: &Vec<f64>| vec![2.0 * x[0]];
241
242        // Gradient function obtained via forward-mode automatic differentiation.
243        get_gradient!(f, g_autodiff);
244
245        // Evaluate the gradient using both functions.
246        let g_eval_autodiff: Vec<f64> = g_autodiff(&x0, &p);
247        let g_eval: Vec<f64> = g(&x0);
248
249        // Test autodiff gradient against true gradient.
250        assert_arrays_equal_to_decimal!(g_eval_autodiff, g_eval, 16);
251    }
252
253    #[test]
254    fn test_gradient_2() {
255        // Function to find the gradient of.
256        fn f<S: Scalar, V: Vector<S>>(x: &V, _p: &[f64]) -> S {
257            x.vget(0).powi(2) + x.vget(1).powi(3)
258        }
259
260        // Evaluation point.
261        let x0: DVector<f64> = DVector::from_slice(&[1.0, 2.0]);
262
263        // Parameter vector (empty for this test).
264        let p = [];
265
266        // True gradient function.
267        let g = |x: &DVector<f64>| DVector::<f64>::from_slice(&[2.0 * x[0], 3.0 * x[1].powi(2)]);
268
269        // Gradient function obtained via forward-mode automatic differentiation.
270        get_gradient!(f, g_autodiff);
271
272        // Evaluate the gradient using both functions.
273        let g_eval_autodiff: DVector<f64> = g_autodiff(&x0, &p);
274        let g_eval: DVector<f64> = g(&x0);
275
276        // Test autodiff gradient against true gradient.
277        assert_arrays_equal_to_decimal!(g_eval_autodiff, g_eval, 16);
278    }
279
280    #[test]
281    fn test_gradient_3() {
282        // Function to find the gradient of.
283        fn f<S: Scalar, V: Vector<S>>(x: &V, _p: &[f64]) -> S {
284            x.vget(0).powi(5) + x.vget(1).sin().powi(3)
285        }
286
287        // Evaluation point.
288        let x0 = array![5.0, 8.0];
289
290        // Parameter vector (empty for this test).
291        let p = [];
292
293        // True gradient function.
294        let g = |x: &Array1<f64>| array![5.0 * x[0].powi(4), 3.0 * x[1].sin().powi(2) * x[1].cos()];
295
296        // Gradient function obtained via forward-mode automatic differentiation.
297        get_gradient!(f, g_autodiff);
298
299        // Evaluate the gradient using both functions.
300        let g_eval_autodiff: Array1<f64> = g_autodiff(&x0, &p);
301        let g_eval: Array1<f64> = g(&x0);
302
303        // Test autodiff gradient against true gradient.
304        assert_arrays_equal_to_decimal!(g_eval_autodiff, g_eval, 16);
305    }
306
307    #[test]
308    fn test_gradient_4() {
309        // Function to find the gradient of.
310        fn f<S: Scalar, V: Vector<S>>(x: &V, p: &[f64]) -> S {
311            let alpha = S::new(p[0]);
312            let beta = S::new(p[1]);
313            let gamma = S::new(p[2]);
314            (alpha * x.vget(0)).exp() * (beta * x.vget(1)).sin() + gamma * x.vget(0) * x.vget(1)
315        }
316
317        // Parameter vector.
318        let p = [0.5, 2.0, 1.2];
319
320        // Evaluation point.
321        let x0 = DVector::from_vec(vec![1.0, 0.5]);
322
323        // True gradient function.
324        let g = |x: &DVector<f64>, p: &[f64]| {
325            let exp_term = (p[0] * x[0]).exp();
326            let sin_term = (p[1] * x[1]).sin();
327            let cos_term = (p[1] * x[1]).cos();
328            DVector::from_vec(vec![
329                p[0] * exp_term * sin_term + p[2] * x[1],
330                exp_term * p[1] * cos_term + p[2] * x[0],
331            ])
332        };
333
334        // Gradient function obtained via forward-mode automatic differentiation.
335        get_gradient!(f, g_autodiff);
336
337        // Evaluate the gradient using both methods.
338        let g_eval: DVector<f64> = g(&x0, &p);
339        let g_eval_autodiff: DVector<f64> = g_autodiff(&x0, &p);
340
341        // Test autodiff gradient against true gradient.
342        assert_arrays_equal_to_decimal!(g_eval_autodiff, g_eval, 14);
343    }
344}