1#[cfg(test)]
2mod test;
3
4use super::{
5 super::{
6 Hessian, Jacobian, Matrix, Scalar, Solution, SquareMatrix, Tensor, Vector,
7 sparse::SparseSolver,
8 },
9 BacktrackingLineSearch, EqualityConstraint, FirstOrderRootFinding, LineSearch,
10 OptimizationError, SecondOrderOptimization,
11};
12use crate::ABS_TOL;
13use crate::math::Norm;
14use std::{
15 fmt::{self, Debug, Formatter},
16 ops::{Div, Mul},
17};
18
19pub struct NewtonRaphson {
21 pub abs_tol: Scalar,
23 pub line_search: LineSearch,
25 pub max_steps: usize,
27 pub norm: Norm,
29}
30
31impl<J, X> BacktrackingLineSearch<J, X> for NewtonRaphson {
32 fn get_line_search(&self) -> &LineSearch {
33 &self.line_search
34 }
35}
36
37impl Debug for NewtonRaphson {
38 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
39 write!(
40 f,
41 "NewtonRaphson {{ abs_tol: {:?}, line_search: {}, max_steps: {:?} }}",
42 self.abs_tol, self.line_search, self.max_steps
43 )
44 }
45}
46
47impl Default for NewtonRaphson {
48 fn default() -> Self {
49 Self {
50 abs_tol: ABS_TOL,
51 line_search: LineSearch::None,
52 max_steps: 25,
53 norm: Norm::Chebyshev,
54 }
55 }
56}
57
58impl<F, J, X> FirstOrderRootFinding<F, J, X> for NewtonRaphson
59where
60 F: Jacobian,
61 for<'a> &'a F: Div<J, Output = X> + From<&'a X>,
62 J: Hessian,
63 X: Solution,
64 for<'a> &'a X: Mul<Scalar, Output = X>,
65 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
66{
67 fn root(
68 &self,
69 function: impl FnMut(&X) -> Result<F, String>,
70 jacobian: impl FnMut(&X) -> Result<J, String>,
71 initial_guess: X,
72 equality_constraint: EqualityConstraint,
73 sparse: Option<SparseSolver>,
74 ) -> Result<X, OptimizationError> {
75 match equality_constraint {
76 EqualityConstraint::Fixed(indices) => constrained_fixed(
77 self,
78 |_: &X| panic!("No line search in root finding"),
79 function,
80 jacobian,
81 initial_guess,
82 sparse,
83 indices,
84 ),
85 EqualityConstraint::Linear(constraint_matrix, constraint_rhs) => constrained(
86 self,
87 |_: &X| panic!("No line search in root finding"),
88 function,
89 jacobian,
90 initial_guess,
91 sparse,
92 constraint_matrix,
93 constraint_rhs,
94 ),
95 EqualityConstraint::None => unconstrained(
96 self,
97 |_: &X| panic!("No line search in root finding"),
98 function,
99 jacobian,
100 initial_guess,
101 ),
102 }
103 }
104}
105
106impl<J, H, X> SecondOrderOptimization<Scalar, J, H, X> for NewtonRaphson
107where
108 H: Hessian,
109 J: Jacobian,
110 for<'a> &'a J: Div<H, Output = X> + From<&'a X>,
111 X: Solution,
112 for<'a> &'a X: Mul<Scalar, Output = X>,
113 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
114{
115 fn minimize(
116 &self,
117 function: impl FnMut(&X) -> Result<Scalar, String>,
118 jacobian: impl FnMut(&X) -> Result<J, String>,
119 hessian: impl FnMut(&X) -> Result<H, String>,
120 initial_guess: X,
121 equality_constraint: EqualityConstraint,
122 sparse: Option<SparseSolver>,
123 ) -> Result<X, OptimizationError> {
124 match match equality_constraint {
125 EqualityConstraint::Fixed(indices) => constrained_fixed(
126 self,
127 function,
128 jacobian,
129 hessian,
130 initial_guess,
131 sparse,
132 indices,
133 ),
134 EqualityConstraint::Linear(constraint_matrix, constraint_rhs) => constrained(
135 self,
136 function,
137 jacobian,
138 hessian,
139 initial_guess,
140 sparse,
141 constraint_matrix,
142 constraint_rhs,
143 ),
144 EqualityConstraint::None => {
145 unconstrained(self, function, jacobian, hessian, initial_guess)
146 }
147 } {
148 Ok(solution) => Ok(solution),
149 Err(error) => Err(OptimizationError::Upstream(
150 format!("{error}"),
151 format!("{self:?}"),
152 )),
153 }
154 }
155}
156
157fn unconstrained<J, H, X>(
158 newton_raphson: &NewtonRaphson,
159 mut function: impl FnMut(&X) -> Result<Scalar, String>,
160 mut jacobian: impl FnMut(&X) -> Result<J, String>,
161 mut hessian: impl FnMut(&X) -> Result<H, String>,
162 initial_guess: X,
163) -> Result<X, OptimizationError>
164where
165 H: Hessian,
166 J: Jacobian,
167 for<'a> &'a J: Div<H, Output = X> + From<&'a X>,
168 X: Solution,
169 for<'a> &'a X: Mul<Scalar, Output = X>,
170{
171 let mut decrement;
172 let mut residual;
173 let mut solution = initial_guess;
174 let mut step_size;
175 let mut tangent;
176 for _ in 0..=newton_raphson.max_steps {
177 residual = jacobian(&solution)?;
178 if newton_raphson.norm.apply(&residual) < newton_raphson.abs_tol {
179 return Ok(solution);
180 } else {
181 tangent = hessian(&solution)?;
182 decrement = &residual / tangent;
183 step_size = newton_raphson.backtracking_line_search(
184 &mut function,
185 &mut jacobian,
186 &solution,
187 &residual,
188 &decrement,
189 1.0,
190 )?;
191 if step_size != 1.0 {
192 decrement *= step_size
193 }
194 solution -= decrement
195 }
196 }
197 Err(OptimizationError::MaximumStepsReached(
198 newton_raphson.max_steps,
199 format!("{:?}", newton_raphson),
200 ))
201}
202
203#[allow(clippy::too_many_arguments)]
204fn constrained_fixed<J, H, X>(
205 newton_raphson: &NewtonRaphson,
206 mut function: impl FnMut(&X) -> Result<Scalar, String>,
207 mut jacobian: impl FnMut(&X) -> Result<J, String>,
208 mut hessian: impl FnMut(&X) -> Result<H, String>,
209 initial_guess: X,
210 sparse: Option<SparseSolver>,
211 indices: Vec<usize>,
212) -> Result<X, OptimizationError>
213where
214 H: Hessian,
215 J: Jacobian,
216 for<'a> &'a J: From<&'a X>,
217 X: Solution,
218 for<'a> &'a X: Mul<Scalar, Output = X>,
219{
220 let mut retained = vec![true; initial_guess.size()];
221 indices.iter().for_each(|&index| retained[index] = false);
222 let unmap: Vec<usize> = retained
223 .iter()
224 .enumerate()
225 .filter_map(|(index, &keep)| keep.then_some(index))
226 .collect();
227 let mut decrement;
228 let mut residual;
229 let mut solution = initial_guess;
230 let mut step_size;
231 for _ in 0..=newton_raphson.max_steps {
232 residual = jacobian(&solution)?.retain_from(&retained);
233 if newton_raphson.norm.apply(&residual) < newton_raphson.abs_tol {
234 return Ok(solution);
235 } else if let Some(ref solver) = sparse {
236 let hess = hessian(&solution)?;
237 decrement = solver.solve(|i, j| hess.entry(unmap[i], unmap[j]), &residual)?
238 } else {
239 decrement = hessian(&solution)?
240 .retain_from(&retained)
241 .solve_lu(&residual)?
242 }
243 if !matches!(newton_raphson.line_search, LineSearch::None) {
244 let jac = jacobian(&solution)?;
245 let mut decrement_full = &solution * 0.0;
246 decrement_full.decrement_from_retained(&retained, &decrement);
247 decrement_full *= -1.0;
248 step_size = newton_raphson.backtracking_line_search(
249 &mut function,
250 &mut jacobian,
251 &solution,
252 &jac,
253 &decrement_full,
254 1.0,
255 )?;
256 if step_size != 1.0 {
257 decrement *= step_size
258 }
259 }
260 solution.decrement_from_retained(&retained, &decrement)
261 }
262 Err(OptimizationError::MaximumStepsReached(
263 newton_raphson.max_steps,
264 format!("{:?}", newton_raphson),
265 ))
266}
267
268#[allow(clippy::too_many_arguments)]
269fn constrained<J, H, X>(
270 newton_raphson: &NewtonRaphson,
271 _function: impl FnMut(&X) -> Result<Scalar, String>,
272 mut jacobian: impl FnMut(&X) -> Result<J, String>,
273 mut hessian: impl FnMut(&X) -> Result<H, String>,
274 initial_guess: X,
275 sparse: Option<SparseSolver>,
276 constraint_matrix: Matrix,
277 constraint_rhs: Vector,
278) -> Result<X, OptimizationError>
279where
280 H: Hessian,
281 J: Jacobian,
282 X: Solution,
283 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
284{
285 if !matches!(newton_raphson.line_search, LineSearch::None) {
286 panic!("Line search needs the exact penalty function in constrained optimization.")
287 }
288 let mut decrement;
289 let num_variables = initial_guess.size();
290 let num_constraints = constraint_rhs.len();
291 let num_total = num_variables + num_constraints;
292 let mut multipliers = Vector::zero(num_constraints);
293 let mut residual = Vector::zero(num_total);
294 let mut solution = initial_guess;
295 let mut tangent = SquareMatrix::zero(if sparse.is_none() { num_total } else { 0 });
296 if sparse.is_none() {
297 constraint_matrix
298 .iter()
299 .enumerate()
300 .for_each(|(i, constraint_matrix_i)| {
301 constraint_matrix_i
302 .iter()
303 .enumerate()
304 .for_each(|(j, constraint_matrix_ij)| {
305 tangent[i + num_variables][j] = -constraint_matrix_ij;
306 tangent[j][i + num_variables] = -constraint_matrix_ij;
307 })
308 });
309 }
310 for _ in 0..=newton_raphson.max_steps {
311 (jacobian(&solution)? - &multipliers * &constraint_matrix).fill_into_chained(
312 &constraint_rhs - &constraint_matrix * &solution,
313 &mut residual,
314 );
315 if newton_raphson.norm.apply(&residual) < newton_raphson.abs_tol {
316 return Ok(solution);
317 } else if let Some(ref solver) = sparse {
318 let hess = hessian(&solution)?;
319 decrement = solver.solve(
320 |i, j| {
321 if i >= num_variables {
322 -constraint_matrix[i - num_variables][j]
323 } else if j >= num_variables {
324 -constraint_matrix[j - num_variables][i]
325 } else {
326 hess.entry(i, j)
327 }
328 },
329 &residual,
330 )?;
331 } else {
332 hessian(&solution)?.fill_into(&mut tangent);
333 decrement = tangent.solve_lu(&residual)?
334 }
335 solution.decrement_from_chained(&mut multipliers, decrement)
336 }
337 Err(OptimizationError::MaximumStepsReached(
338 newton_raphson.max_steps,
339 format!("{:?}", newton_raphson),
340 ))
341}