1#[cfg(test)]
2mod test;
3
4use super::{
5 super::{Jacobian, Matrix, Scalar, Solution, Tensor, Vector},
6 BacktrackingLineSearch, EqualityConstraint, FirstOrderOptimization, LineSearch,
7 OptimizationError, StepSize, Tolerances, ZerothOrderRootFinding,
8};
9use crate::math::{Erase, Is, Norm};
10use crate::units::{UnitDiv, UnitMul, UnitSum};
11use std::{
12 fmt::{self, Debug, Formatter},
13 ops::Mul,
14};
15
16const CUTBACK_FACTOR: Scalar = 0.8;
17const CUTBACK_FACTOR_MINUS_ONE: Scalar = 1.0 - CUTBACK_FACTOR;
18const INITIAL_STEP_SIZE: Scalar = 1e-2;
19
20pub struct GradientDescent {
22 pub abs_tol: Tolerances,
24 pub dual: bool,
26 pub error_norm: Norm,
28 pub line_search: LineSearch,
30 pub max_steps: usize,
32 pub rel_tol: Option<Scalar>,
34}
35
36impl<J, X> BacktrackingLineSearch<J, X> for GradientDescent {
37 fn get_line_search(&self) -> &LineSearch {
38 &self.line_search
39 }
40}
41
42impl Debug for GradientDescent {
43 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44 write!(
45 f,
46 "GradientDescent {{ abs_tol: {:?}, dual: {:?}, line_search: {}, max_steps: {:?}, rel_tol: {:?} }}",
47 self.abs_tol, self.dual, self.line_search, self.max_steps, self.rel_tol
48 )
49 }
50}
51
52impl Default for GradientDescent {
53 fn default() -> Self {
54 Self {
55 abs_tol: Tolerances::default(),
56 dual: false,
57 error_norm: Norm::Chebyshev,
58 line_search: LineSearch::None,
59 max_steps: 250,
60 rel_tol: None,
61 }
62 }
63}
64
65impl<F, X, E> ZerothOrderRootFinding<F, X> for GradientDescent
66where
67 F: Erase<Erased = E> + Jacobian + Mul<StepSize<F, X>, Output = X>,
68 for<'a> &'a F: Mul<StepSize<F, X>, Output = X>,
69 X: Erase<Erased = E> + Jacobian + Solution,
70 <X as Tensor>::Unit: UnitDiv<<F as Tensor>::Unit>,
71 E: Tensor,
72 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
73{
74 fn root(
75 &self,
76 function: impl FnMut(&X) -> Result<F, String>,
77 initial_guess: X,
78 equality_constraint: EqualityConstraint,
79 ) -> Result<X, OptimizationError> {
80 match equality_constraint {
81 EqualityConstraint::Fixed(indices) => constrained_fixed(
82 self,
83 |_: &X| panic!("No line search in root finding."),
84 function,
85 initial_guess,
86 indices,
87 ),
88 EqualityConstraint::Linear(constraint_matrix, constraint_rhs) => {
89 if self.dual {
90 constrained_dual(
91 self,
92 function,
93 initial_guess,
94 constraint_matrix,
95 constraint_rhs,
96 )
97 } else {
98 constrained(
99 self,
100 function,
101 initial_guess,
102 constraint_matrix,
103 constraint_rhs,
104 )
105 }
106 }
107 EqualityConstraint::None => unconstrained(
108 self,
109 |_: &X| panic!("No line search in root finding."),
110 function,
111 initial_guess,
112 None,
113 ),
114 }
115 }
116}
117
118impl<F, J, X, E> FirstOrderOptimization<F, J, X> for GradientDescent
119where
120 F: Erase<Erased = Scalar> + Tensor,
121 <J as Tensor>::Unit: UnitMul<<X as Tensor>::Unit>,
122 <<J as Tensor>::Unit as UnitMul<<X as Tensor>::Unit>>::Output: UnitSum,
123 <<<J as Tensor>::Unit as UnitMul<<X as Tensor>::Unit>>::Output as UnitSum>::Output:
124 Is<<F as Tensor>::Unit>,
125 J: Erase<Erased = E> + Jacobian + Mul<StepSize<J, X>, Output = X>,
126 for<'a> &'a J: Mul<StepSize<J, X>, Output = X>,
127 X: Erase<Erased = E> + Jacobian + Solution,
128 <X as Tensor>::Unit: UnitDiv<<J as Tensor>::Unit>,
129 E: Tensor,
130 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
131{
132 fn minimize(
133 &self,
134 mut function: impl FnMut(&X) -> Result<F, String>,
135 jacobian: impl FnMut(&X) -> Result<J, String>,
136 initial_guess: X,
137 equality_constraint: EqualityConstraint,
138 ) -> Result<X, OptimizationError> {
139 let objective = move |argument: &X| function(argument).map(|value| *value.erase());
140 match equality_constraint {
141 EqualityConstraint::Fixed(indices) => {
142 constrained_fixed(self, objective, jacobian, initial_guess, indices)
143 }
144 EqualityConstraint::Linear(constraint_matrix, constraint_rhs) => {
145 if self.dual {
146 constrained_dual(
147 self,
148 jacobian,
149 initial_guess,
150 constraint_matrix,
151 constraint_rhs,
152 )
153 } else {
154 constrained(
155 self,
156 jacobian,
157 initial_guess,
158 constraint_matrix,
159 constraint_rhs,
160 )
161 }
162 }
163 EqualityConstraint::None => {
164 unconstrained(self, objective, jacobian, initial_guess, None)
165 }
166 }
167 }
168}
169
170fn unconstrained<F, X, E>(
171 gradient_descent: &GradientDescent,
172 mut function: impl FnMut(&X) -> Result<Scalar, String>,
173 mut jacobian: impl FnMut(&X) -> Result<F, String>,
174 initial_guess: X,
175 linear_equality_constraint: Option<(&Matrix, &Vector)>,
176) -> Result<X, OptimizationError>
177where
178 F: Erase<Erased = E> + Jacobian + Mul<StepSize<F, X>, Output = X>,
179 for<'a> &'a F: Mul<StepSize<F, X>, Output = X>,
180 X: Erase<Erased = E> + Jacobian + Solution,
181 <X as Tensor>::Unit: UnitDiv<<F as Tensor>::Unit>,
182 E: Tensor,
183{
184 let constraint = if let Some((constraint_matrix, multipliers)) = linear_equality_constraint {
185 Some(multipliers * constraint_matrix)
186 } else {
187 None
188 };
189 let mut residual;
190 let mut residual_change = None;
191 let mut solution = initial_guess.clone();
192 let mut solution_change = solution.clone();
193 let mut step_size = INITIAL_STEP_SIZE;
194 let mut step_trial;
195 let mut steps = 0;
196 loop {
197 residual = if let Some(ref extra) = constraint {
198 jacobian(&solution)? - extra
199 } else {
200 jacobian(&solution)?
201 };
202 if gradient_descent.error_norm.apply(&residual) < gradient_descent.abs_tol.residual() {
203 return Ok(solution);
204 } else if steps == gradient_descent.max_steps {
205 return Err(OptimizationError::MaximumStepsReached(
206 gradient_descent.max_steps,
207 format!("{gradient_descent:?}"),
208 ));
209 } else {
210 steps += 1;
211 solution_change -= &solution;
212 let change = residual_change.get_or_insert_with(|| zeroed(&residual));
213 *change -= &residual;
214 step_trial = change.erase().full_contraction(solution_change.erase())
215 / change.erase().full_contraction(change.erase());
216 if step_trial.abs() > 0.0 && !step_trial.is_nan() {
217 step_size = step_trial.abs()
218 }
219 step_size = gradient_descent.backtracking_line_search::<F, E>(
220 |trial: &X, _: Scalar| function(trial),
221 &mut jacobian,
222 &solution,
223 &residual,
224 &residual,
225 step_size,
226 )?;
227 *change = residual.clone();
228 solution_change = solution.clone();
229 solution -= residual * StepSize::<F, X>::new(step_size);
230 }
231 }
232}
233
234fn constrained_fixed<F, X, E>(
235 gradient_descent: &GradientDescent,
236 mut function: impl FnMut(&X) -> Result<Scalar, String>,
237 mut jacobian: impl FnMut(&X) -> Result<F, String>,
238 initial_guess: X,
239 indices: Vec<usize>,
240) -> Result<X, OptimizationError>
241where
242 F: Erase<Erased = E> + Jacobian + Mul<StepSize<F, X>, Output = X>,
243 for<'a> &'a F: Mul<StepSize<F, X>, Output = X>,
244 X: Erase<Erased = E> + Jacobian + Solution,
245 <X as Tensor>::Unit: UnitDiv<<F as Tensor>::Unit>,
246 E: Tensor,
247{
248 let mut relative_scale = 0.0;
249 let mut residual: F;
250 let mut residual_change = None;
251 let mut residual_norm;
252 let mut solution = initial_guess.clone();
253 let mut solution_change = solution.clone();
254 let mut step_size = INITIAL_STEP_SIZE;
255 let mut step_trial;
256 let mut steps = 0;
257 loop {
258 residual = jacobian(&solution)?;
259 residual.zero_out(&indices);
260 residual_norm = gradient_descent.error_norm.measure(&residual);
261 if gradient_descent.rel_tol.is_some() && steps == 0 {
262 relative_scale = gradient_descent.error_norm.measure(&residual)
263 }
264 if residual_norm < gradient_descent.abs_tol.residual {
265 return Ok(solution);
266 } else if let Some(rel_tol) = gradient_descent.rel_tol
267 && residual_norm / relative_scale < rel_tol
268 {
269 return Ok(solution);
270 } else if steps == gradient_descent.max_steps {
271 return Err(OptimizationError::MaximumStepsReached(
272 gradient_descent.max_steps,
273 format!("{gradient_descent:?}"),
274 ));
275 } else {
276 steps += 1;
277 solution_change -= &solution;
278 let change = residual_change.get_or_insert_with(|| zeroed(&residual));
279 *change -= &residual;
280 step_trial = change.erase().full_contraction(solution_change.erase())
281 / change.erase().full_contraction(change.erase());
282 if step_trial.abs() > 0.0 && !step_trial.is_nan() {
283 step_size = step_trial.abs()
284 }
285 step_size = gradient_descent.backtracking_line_search::<F, E>(
286 |trial: &X, _: Scalar| function(trial),
287 &mut jacobian,
288 &solution,
289 &residual,
290 &residual,
291 step_size,
292 )?;
293 *change = residual.clone();
294 solution_change = solution.clone();
295 solution -= residual * StepSize::<F, X>::new(step_size);
296 }
297 }
298}
299
300fn constrained<F, X, E>(
301 gradient_descent: &GradientDescent,
302 mut jacobian: impl FnMut(&X) -> Result<F, String>,
303 initial_guess: X,
304 constraint_matrix: Matrix,
305 constraint_rhs: Vector,
306) -> Result<X, OptimizationError>
307where
308 F: Erase<Erased = E> + Jacobian + Mul<StepSize<F, X>, Output = X>,
309 X: Erase<Erased = E> + Jacobian,
310 <X as Tensor>::Unit: UnitDiv<<F as Tensor>::Unit>,
311 E: Tensor,
312 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
313{
314 if !matches!(gradient_descent.line_search, LineSearch::None) {
315 panic!("Line search needs the exact penalty function in constrained optimization.")
316 }
317 let mut residual_solution;
318 let mut residual_solution_change = None;
319 let mut solution = initial_guess.clone();
320 let mut solution_change = solution.clone();
321 let mut step_size_solution = INITIAL_STEP_SIZE;
322 let mut step_trial_solution;
323 let num_constraints = constraint_rhs.len();
324 let mut residual_multipliers;
325 let mut residual_multipliers_change = Vector::zero(num_constraints);
326 let mut multipliers = Vector::zero(num_constraints);
327 let mut multipliers_change = Vector::zero(num_constraints);
328 let mut step_size_multipliers = INITIAL_STEP_SIZE;
329 let mut step_trial_multipliers;
330 let mut step_size;
331 let mut steps = 0;
332 loop {
333 residual_solution = jacobian(&solution)? - &multipliers * &constraint_matrix;
334 residual_multipliers = &constraint_rhs - &constraint_matrix * &solution;
335 if gradient_descent.error_norm.apply(&residual_solution)
336 < gradient_descent.abs_tol.residual()
337 && gradient_descent.error_norm.apply(&residual_multipliers)
338 < gradient_descent.abs_tol.constraint()
339 {
340 return Ok(solution);
341 } else if steps == gradient_descent.max_steps {
342 return Err(OptimizationError::MaximumStepsReached(
343 gradient_descent.max_steps,
344 format!("{gradient_descent:?}"),
345 ));
346 } else {
347 steps += 1;
348 solution_change -= &solution;
349 let change = residual_solution_change.get_or_insert_with(|| zeroed(&residual_solution));
350 *change -= &residual_solution;
351 step_trial_solution = change.erase().full_contraction(solution_change.erase())
352 / change.erase().full_contraction(change.erase());
353 if step_trial_solution.abs() > 0.0 && !step_trial_solution.is_nan() {
354 step_size_solution = step_trial_solution.abs()
355 }
356 *change = residual_solution.clone();
357 solution_change = solution.clone();
358 multipliers_change -= &multipliers;
359 residual_multipliers_change -= &residual_multipliers;
360 step_trial_multipliers = residual_multipliers_change
361 .full_contraction(&multipliers_change)
362 / residual_multipliers_change.full_contraction(&residual_multipliers_change);
363 if step_trial_multipliers.abs() > 0.0 && !step_trial_multipliers.is_nan() {
364 step_size_multipliers = step_trial_multipliers.abs()
365 }
366 residual_multipliers_change = residual_multipliers.clone();
367 multipliers_change = multipliers.clone();
368 step_size = step_size_solution.min(step_size_multipliers);
369 solution -= residual_solution * StepSize::<F, X>::new(step_size);
370 multipliers += residual_multipliers * step_size;
371 }
372 }
373}
374
375fn constrained_dual<F, X, E>(
376 gradient_descent: &GradientDescent,
377 mut jacobian: impl FnMut(&X) -> Result<F, String>,
378 initial_guess: X,
379 constraint_matrix: Matrix,
380 constraint_rhs: Vector,
381) -> Result<X, OptimizationError>
382where
383 F: Erase<Erased = E> + Jacobian + Mul<StepSize<F, X>, Output = X>,
384 for<'a> &'a F: Mul<StepSize<F, X>, Output = X>,
385 X: Erase<Erased = E> + Jacobian + Solution,
386 <X as Tensor>::Unit: UnitDiv<<F as Tensor>::Unit>,
387 E: Tensor,
388 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
389{
390 if !matches!(gradient_descent.line_search, LineSearch::None) {
391 panic!("Line search needs the exact penalty function in constrained optimization.")
392 }
393 let num_constraints = constraint_rhs.len();
394 let mut multipliers = Vector::zero(num_constraints);
395 let mut multipliers_change = multipliers.clone();
396 let mut residual;
397 let mut residual_change = Vector::zero(num_constraints);
398 let mut solution = initial_guess;
399 let mut step_size = INITIAL_STEP_SIZE;
400 let mut step_trial;
401 for _ in 0..gradient_descent.max_steps {
402 if let Ok(result) = unconstrained(
403 gradient_descent,
404 |_: &X| {
405 panic!("Line search needs the exact penalty function in constrained optimization.")
406 },
407 &mut jacobian,
408 solution.clone(),
409 Some((&constraint_matrix, &multipliers)),
410 ) {
411 solution = result;
412 residual = &constraint_rhs - &constraint_matrix * &solution;
413 if gradient_descent.error_norm.apply(&residual) < gradient_descent.abs_tol.constraint()
414 {
415 return Ok(solution);
416 } else {
417 multipliers_change -= &multipliers;
418 residual_change -= &residual;
419 step_trial = residual_change.full_contraction(&multipliers_change)
420 / residual_change.full_contraction(&residual_change);
421 if step_trial.abs() > 0.0 && !step_trial.is_nan() {
422 step_size = step_trial.abs()
423 }
424 residual_change = residual.clone();
425 multipliers_change = multipliers.clone();
426 multipliers += residual * step_size;
427 }
428 } else {
429 multipliers -= (multipliers.clone() - &multipliers_change) * CUTBACK_FACTOR_MINUS_ONE;
433 step_size *= CUTBACK_FACTOR;
434 }
435 }
436 Err(OptimizationError::MaximumStepsReached(
437 gradient_descent.max_steps,
438 format!("{gradient_descent:?}"),
439 ))
440}
441
442fn zeroed<F>(residual: &F) -> F
444where
445 F: Tensor,
446{
447 let mut zero = residual.clone();
448 zero *= 0.0;
449 zero
450}