fenris_optimize/
calculus.rs

1use fenris_traits::Real;
2use nalgebra::{DMatrix, DMatrixViewMut, DVector, DVectorView, DVectorViewMut, Dim, DimName, Dyn, Scalar, Vector, U1};
3
4use nalgebra::base::storage::{Storage, StorageMut};
5use numeric_literals::replace_float_literals;
6use std::error::Error;
7
8pub trait VectorFunction<T>
9where
10    T: Scalar,
11{
12    fn dimension(&self) -> usize;
13    fn eval_into(&mut self, f: &mut DVectorViewMut<T>, x: &DVectorView<T>);
14}
15
16impl<T, X> VectorFunction<T> for &mut X
17where
18    T: Scalar,
19    X: VectorFunction<T>,
20{
21    fn dimension(&self) -> usize {
22        X::dimension(self)
23    }
24
25    fn eval_into(&mut self, f: &mut DVectorViewMut<T>, x: &DVectorView<T>) {
26        X::eval_into(self, f, x)
27    }
28}
29
30pub trait DifferentiableVectorFunction<T>: VectorFunction<T>
31where
32    T: Scalar,
33{
34    fn solve_jacobian_system(
35        &mut self,
36        sol: &mut DVectorViewMut<T>,
37        x: &DVectorView<T>,
38        rhs: &DVectorView<T>,
39    ) -> Result<(), Box<dyn Error>>;
40}
41
42impl<T, X> DifferentiableVectorFunction<T> for &mut X
43where
44    T: Scalar,
45    X: DifferentiableVectorFunction<T>,
46{
47    fn solve_jacobian_system(
48        &mut self,
49        sol: &mut DVectorViewMut<T>,
50        x: &DVectorView<T>,
51        rhs: &DVectorView<T>,
52    ) -> Result<(), Box<dyn Error>> {
53        X::solve_jacobian_system(self, sol, x, rhs)
54    }
55}
56
57#[derive(Debug, Clone)]
58pub struct VectorFunctionBuilder {
59    dimension: usize,
60}
61
62#[derive(Debug, Clone)]
63pub struct ConcreteVectorFunction<F, J> {
64    dimension: usize,
65    function: F,
66    jacobian_solver: J,
67}
68
69impl VectorFunctionBuilder {
70    pub fn with_dimension(dimension: usize) -> Self {
71        Self { dimension }
72    }
73
74    pub fn with_function<F, T>(self, function: F) -> ConcreteVectorFunction<F, ()>
75    where
76        T: Scalar,
77        F: FnMut(&mut DVectorViewMut<T>, &DVectorView<T>),
78    {
79        ConcreteVectorFunction {
80            dimension: self.dimension,
81            function,
82            jacobian_solver: (),
83        }
84    }
85}
86
87impl<F> ConcreteVectorFunction<F, ()> {
88    pub fn with_jacobian_solver<J, T>(self, jacobian_solver: J) -> ConcreteVectorFunction<F, J>
89    where
90        T: Scalar,
91        J: FnMut(&mut DVectorViewMut<T>, &DVectorView<T>, &DVectorView<T>) -> Result<(), Box<dyn Error>>,
92    {
93        ConcreteVectorFunction {
94            dimension: self.dimension,
95            function: self.function,
96            jacobian_solver,
97        }
98    }
99}
100
101impl<F, J, T> VectorFunction<T> for ConcreteVectorFunction<F, J>
102where
103    T: Scalar,
104    F: FnMut(&mut DVectorViewMut<T>, &DVectorView<T>),
105{
106    fn dimension(&self) -> usize {
107        self.dimension
108    }
109
110    fn eval_into(&mut self, f: &mut DVectorViewMut<T>, x: &DVectorView<T>) {
111        let func = &mut self.function;
112        func(f, x)
113    }
114}
115
116impl<F, J, T> DifferentiableVectorFunction<T> for ConcreteVectorFunction<F, J>
117where
118    T: Scalar,
119    F: FnMut(&mut DVectorViewMut<T>, &DVectorView<T>),
120    J: FnMut(&mut DVectorViewMut<T>, &DVectorView<T>, &DVectorView<T>) -> Result<(), Box<dyn Error>>,
121{
122    fn solve_jacobian_system(
123        &mut self,
124        sol: &mut DVectorViewMut<T>,
125        x: &DVectorView<T>,
126        rhs: &DVectorView<T>,
127    ) -> Result<(), Box<dyn Error>> {
128        let j = &mut self.jacobian_solver;
129        j(sol, x, rhs)
130    }
131}
132
133// TODO: Move somewhere else? Ideally contribute as From<_> for DVectorView<T> in `nalgebra`
134fn as_vector_slice<T, R, S>(vector: &Vector<T, R, S>) -> DVectorView<T>
135where
136    T: Scalar,
137    S: Storage<T, R, U1, RStride = U1, CStride = Dyn>,
138    R: Dim,
139{
140    vector.generic_view((0, 0), (Dyn(vector.nrows()), U1::name()))
141}
142
143// TODO: Move somewhere else? Ideally contribute as From<_> for DVectorViewMut<T> in `nalgebra`
144fn as_vector_slice_mut<T, R, S>(vector: &mut Vector<T, R, S>) -> DVectorViewMut<T>
145where
146    T: Scalar,
147    S: StorageMut<T, R, U1, RStride = U1, CStride = Dyn>,
148    R: Dim,
149{
150    vector.generic_view_mut((0, 0), (Dyn(vector.nrows()), U1::name()))
151}
152
153/// Approximates the Jacobian of a vector function evaluated at `x`, using
154/// central finite differences with resolution `h`.
155#[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))]
156pub fn approximate_jacobian<T>(mut f: impl VectorFunction<T>, x: &DVector<T>, h: &T) -> DMatrix<T>
157where
158    T: Real,
159{
160    let out_dim = f.dimension();
161    let in_dim = x.len();
162
163    let mut result = DMatrix::zeros(out_dim, in_dim);
164
165    // Define quantities x+ and x- as follows:
166    //  x+ := x + h e_j
167    //  x- := x - h e_j
168    // where e_j is the jth basis vector consisting of all zeros except for the j-th element,
169    // which is 1.
170    let mut x_plus = x.clone();
171    let mut x_minus = x.clone();
172
173    // f+ := f(x+)
174    // f- := f(x-)
175    let mut f_plus = DVector::zeros(out_dim);
176    let mut f_minus = DVector::zeros(out_dim);
177
178    // Use finite differences to compute a numerical approximation of the Jacobian
179    for j in 0..in_dim {
180        // TODO: Can optimize this a little by simple resetting the element at the end of the iteration
181        x_plus.copy_from(x);
182        x_plus[j] += *h;
183        x_minus.copy_from(x);
184        x_minus[j] -= *h;
185
186        f.eval_into(&mut as_vector_slice_mut(&mut f_plus), &as_vector_slice(&x_plus));
187        f.eval_into(&mut as_vector_slice_mut(&mut f_minus), &as_vector_slice(&x_minus));
188
189        // result[.., j] := (f+ - f-) / 2h
190        let mut column_j = result.column_mut(j);
191        column_j += &f_plus;
192        column_j -= &f_minus;
193        column_j /= 2.0 * *h;
194    }
195
196    result
197}
198
199/// Approximates the derivative of the function `f: R^n -> R` with finite differences.
200///
201/// The parameter `h` determines the step size of the finite difference approximation.
202///
203/// The vector `x` is mutable in order to contain intermediate computations, but upon returning,
204/// its content remains unchanged.
205pub fn approximate_gradient_fd<'a, T>(
206    f: impl FnMut(DVectorView<T>) -> T,
207    x: impl Into<DVectorViewMut<'a, T>>,
208    h: T,
209) -> DVector<T>
210where
211    T: Real,
212{
213    let x = x.into();
214    let mut df = DVector::zeros(x.len());
215    approximate_gradient_fd_into_(DVectorViewMut::from(&mut df), f, x, h);
216    df
217}
218
219/// Approximates the derivative of the function `f: R^n -> R` with finite differences.
220///
221/// Analogous to [`approximate_gradient_fd`], but stores the result in the provided
222/// output vector.
223///
224/// The vector `x` is mutable in order to contain intermediate results, but upon returning,
225/// its content remains unchanged.
226pub fn approximate_gradient_fd_into<'a, T>(
227    mut df: DVectorViewMut<T>,
228    f: impl FnMut(DVectorView<T>) -> T,
229    x: impl Into<DVectorViewMut<'a, T>>,
230    h: T,
231) where
232    T: Real,
233{
234    approximate_gradient_fd_into_(DVectorViewMut::from(&mut df), f, x.into(), h);
235}
236
237#[replace_float_literals(T::from_f64(literal).unwrap())]
238fn approximate_gradient_fd_into_<T>(
239    mut df: DVectorViewMut<T>,
240    mut f: impl FnMut(DVectorView<T>) -> T,
241    mut x: DVectorViewMut<T>,
242    h: T,
243) where
244    T: Real,
245{
246    let n = x.len();
247    for i in 0..n {
248        let x_i = x[i];
249        x[i] = x_i + h;
250        let f_plus = f(DVectorView::from(&x));
251        x[i] = x_i - h;
252        let f_minus = f(DVectorView::from(&x));
253        let df_i = (f_plus - f_minus) / (2.0 * h);
254        df[i] = df_i;
255        x[i] = x_i;
256    }
257}
258
259/// Approximates the Jacobian of the function $f: \mathbb{R}^n \rightarrow \mathbb{R}^m$
260/// with finite differences.
261///
262/// The Jacobian matrix is the $m \times n$ matrix whose entries are given by
263/// $$ J_{ij} := \pd{f_i}{x_j}.$$
264///
265/// The parameter `h` determines the step size of the finite difference approximation.
266pub fn approximate_jacobian_fd<'a, T>(
267    m: usize,
268    f: impl FnMut(DVectorView<T>, DVectorViewMut<T>),
269    x: impl Into<DVectorViewMut<'a, T>>,
270    h: T,
271) -> DMatrix<T>
272where
273    T: Real,
274{
275    let x = x.into();
276    let n = x.len();
277    let mut jacobian = DMatrix::zeros(m, n);
278    approximate_jacobian_fd_into_(DMatrixViewMut::from(&mut jacobian), f, x, h);
279    jacobian
280}
281
282/// Approximates the Jacobian of the function $f: \mathbb{R}^n \rightarrow \mathbb{R}^m$
283/// with finite differences.
284///
285/// Same as [`approximate_jacobian_fd`], but stores the result in the provided output matrix.
286pub fn approximate_jacobian_fd_into<'a, T>(
287    jacobian: impl Into<DMatrixViewMut<'a, T>>,
288    f: impl FnMut(DVectorView<T>, DVectorViewMut<T>),
289    x: impl Into<DVectorViewMut<'a, T>>,
290    h: T,
291) where
292    T: Real,
293{
294    approximate_jacobian_fd_into_(jacobian.into(), f, x.into(), h);
295}
296
297#[replace_float_literals(T::from_f64(literal).unwrap())]
298fn approximate_jacobian_fd_into_<T>(
299    mut j: DMatrixViewMut<T>,
300    mut f: impl FnMut(DVectorView<T>, DVectorViewMut<T>),
301    mut x: DVectorViewMut<T>,
302    h: T,
303) where
304    T: Real,
305{
306    let m = j.nrows();
307    let n = x.len();
308    assert_eq!(n, j.ncols());
309
310    // Buffers to hold f(x + e_i h) and f(x - e_i h)
311    let mut f_plus = DVector::zeros(m);
312    let mut f_minus = DVector::zeros(m);
313
314    // Jacobian has m x n rows
315    // Build column by column
316    for i in 0..n {
317        // df_dxi ~ (f(x + h e_i) - f(x - h e_i)) / (2 h)
318        let xi = x[i];
319        x[i] = xi + h;
320        f(DVectorView::from(&x), DVectorViewMut::from(&mut f_plus));
321        x[i] = xi - h;
322        f(DVectorView::from(&x), DVectorViewMut::from(&mut f_minus));
323        x[i] = xi;
324
325        let mut df_dxi = j.column_mut(i);
326        df_dxi.copy_from(&f_plus);
327        df_dxi -= &f_minus;
328        df_dxi /= 2.0 * h;
329    }
330}