1#[cfg(test)]
2mod test;
3
4use super::{
5 super::{
6 Erase, Hessian, HessianBlock, Is, Jacobian, LuDecomposition, Matrix, Quantity, Scalar,
7 Solution, SquareMatrix, Tensor, Vector,
8 sparse::{CscMatrix, SparseSolver},
9 },
10 BacktrackingLineSearch, EqualityConstraint, FirstOrderRootFinding, FirstOrderRootFindingBlock,
11 FirstOrderRootFindingIncremental, LineSearch, LineSearchError, OptimizationError,
12 SecondOrderOptimization, SecondOrderOptimizationBlock, SecondOrderOptimizationIncremental,
13 SolveStrategy, Tolerances, TrustRegion,
14};
15use crate::math::Norm;
16use crate::units::{Dimensionless, UnitDiv, UnitMul, UnitSum};
17use std::{
18 fmt::{self, Debug, Formatter},
19 ops::{Div, Mul},
20};
21
22#[derive(Clone)]
24pub struct NewtonRaphson {
25 pub abs_tol: Tolerances,
27 pub error_norm: Norm,
29 pub line_search: LineSearch,
31 pub max_steps: usize,
33 pub rel_tol: Option<Scalar>,
35 pub trust_region: TrustRegion,
37}
38
39impl<J, X> BacktrackingLineSearch<J, X> for NewtonRaphson {
40 fn get_line_search(&self) -> &LineSearch {
41 &self.line_search
42 }
43}
44
45impl Debug for NewtonRaphson {
46 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
47 write!(
48 f,
49 "NewtonRaphson {{ abs_tol: {:?}, line_search: {}, max_steps: {:?}, rel_tol: {:?}, trust_region: {:?} }}",
50 self.abs_tol, self.line_search, self.max_steps, self.rel_tol, self.trust_region
51 )
52 }
53}
54
55impl Default for NewtonRaphson {
56 fn default() -> Self {
57 Self {
58 abs_tol: Tolerances::default(),
59 error_norm: Norm::Chebyshev,
60 line_search: LineSearch::None,
61 max_steps: 25,
62 rel_tol: None,
63 trust_region: TrustRegion::None,
64 }
65 }
66}
67
68impl<F, J, X, E> FirstOrderRootFinding<F, J, X> for NewtonRaphson
69where
70 F: Jacobian,
71 for<'a> &'a F: Div<J, Output = X>,
72 J: Hessian,
73 F: Erase<Erased = E>,
74 X: Erase<Erased = E> + Solution,
75 E: Tensor,
76 <X as Tensor>::Unit: UnitDiv<<X as Tensor>::Unit, Output = Dimensionless>,
77 for<'a> &'a X: Mul<Quantity<Dimensionless>, Output = X> + Mul<Scalar, Output = X>,
78 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
79{
80 fn root(
81 &self,
82 function: impl FnMut(&X) -> Result<F, String>,
83 jacobian: impl FnMut(&X) -> Result<J, String>,
84 initial_guess: X,
85 equality_constraint: EqualityConstraint,
86 sparse: Option<SparseSolver>,
87 ) -> Result<X, OptimizationError> {
88 match equality_constraint {
89 EqualityConstraint::Fixed(indices) => constrained_fixed(
90 self,
91 |_: &X| panic!("No line search in root finding"),
92 function,
93 jacobian,
94 |_: &X, _: &Vector, _: Scalar, _: bool| Ok(()),
95 initial_guess,
96 sparse,
97 indices,
98 ),
99 EqualityConstraint::Linear(constraint_matrix, constraint_rhs) => constrained(
100 self,
101 |_: &X| panic!("No line search in root finding"),
102 function,
103 jacobian,
104 |_: &X, _: &Vector, _: Scalar, _: bool| Ok(()),
105 initial_guess,
106 sparse,
107 constraint_matrix,
108 constraint_rhs,
109 ),
110 EqualityConstraint::None => unconstrained(
111 self,
112 |_: &X| panic!("No line search in root finding"),
113 function,
114 jacobian,
115 initial_guess,
116 sparse,
117 ),
118 }
119 .map_err(|error| OptimizationError::upstream(error, self))
120 }
121}
122
123impl<F, J, X, E> FirstOrderRootFindingIncremental<F, J, X> for NewtonRaphson
124where
125 F: Jacobian,
126 for<'a> &'a F: Div<J, Output = X>,
127 J: Hessian,
128 F: Erase<Erased = E>,
129 X: Erase<Erased = E> + Solution,
130 E: Tensor,
131 <X as Tensor>::Unit: UnitDiv<<X as Tensor>::Unit, Output = Dimensionless>,
132 for<'a> &'a X: Mul<Quantity<Dimensionless>, Output = X> + Mul<Scalar, Output = X>,
133 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
134{
135 fn root_incremental(
136 &self,
137 function: impl FnMut(&X) -> Result<F, String>,
138 jacobian: impl FnMut(&X) -> Result<J, String>,
139 update: impl FnMut(&X, &Vector, Scalar, bool) -> Result<(), String>,
140 initial_guess: X,
141 equality_constraint: EqualityConstraint,
142 sparse: Option<SparseSolver>,
143 ) -> Result<X, OptimizationError> {
144 match equality_constraint {
145 EqualityConstraint::Fixed(indices) => constrained_fixed(
146 self,
147 |_: &X| panic!("No line search in root finding"),
148 function,
149 jacobian,
150 update,
151 initial_guess,
152 sparse,
153 indices,
154 ),
155 EqualityConstraint::Linear(constraint_matrix, constraint_rhs) => constrained(
156 self,
157 |_: &X| panic!("No line search in root finding"),
158 function,
159 jacobian,
160 update,
161 initial_guess,
162 sparse,
163 constraint_matrix,
164 constraint_rhs,
165 ),
166 EqualityConstraint::None => unimplemented!(
167 "An unconstrained solution has no chained vector to lend the increment through."
168 ),
169 }
170 .map_err(|error| OptimizationError::upstream(error, self))
171 }
172}
173
174impl<F, J, H, X, E> SecondOrderOptimization<F, J, H, X> for NewtonRaphson
175where
176 F: Erase<Erased = Scalar> + Tensor,
177 <J as Tensor>::Unit: UnitMul<<X as Tensor>::Unit>,
178 <<J as Tensor>::Unit as UnitMul<<X as Tensor>::Unit>>::Output: UnitSum,
179 <<<J as Tensor>::Unit as UnitMul<<X as Tensor>::Unit>>::Output as UnitSum>::Output:
180 Is<<F as Tensor>::Unit>,
181 H: Hessian,
182 J: Jacobian,
183 for<'a> &'a J: Div<H, Output = X>,
184 J: Erase<Erased = E>,
185 X: Erase<Erased = E> + Solution,
186 E: Tensor,
187 <X as Tensor>::Unit: UnitDiv<<X as Tensor>::Unit, Output = Dimensionless>,
188 for<'a> &'a X: Mul<Quantity<Dimensionless>, Output = X> + Mul<Scalar, Output = X>,
189 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
190{
191 fn minimize(
192 &self,
193 mut function: impl FnMut(&X) -> Result<F, String>,
194 jacobian: impl FnMut(&X) -> Result<J, String>,
195 hessian: impl FnMut(&X) -> Result<H, String>,
196 initial_guess: X,
197 equality_constraint: EqualityConstraint,
198 sparse: Option<SparseSolver>,
199 ) -> Result<X, OptimizationError> {
200 let function = move |argument: &X| function(argument).map(|value| *value.erase());
201 match equality_constraint {
202 EqualityConstraint::Fixed(indices) => constrained_fixed(
203 self,
204 function,
205 jacobian,
206 hessian,
207 |_: &X, _: &Vector, _: Scalar, _: bool| Ok(()),
208 initial_guess,
209 sparse,
210 indices,
211 ),
212 EqualityConstraint::Linear(constraint_matrix, constraint_rhs) => constrained(
213 self,
214 function,
215 jacobian,
216 hessian,
217 |_: &X, _: &Vector, _: Scalar, _: bool| Ok(()),
218 initial_guess,
219 sparse,
220 constraint_matrix,
221 constraint_rhs,
222 ),
223 EqualityConstraint::None => {
224 unconstrained(self, function, jacobian, hessian, initial_guess, sparse)
225 }
226 }
227 .map_err(|error| OptimizationError::upstream(error, self))
228 }
229}
230
231impl<F, J, H, X, E> SecondOrderOptimizationIncremental<F, J, H, X> for NewtonRaphson
232where
233 F: Erase<Erased = Scalar> + Tensor,
234 <J as Tensor>::Unit: UnitMul<<X as Tensor>::Unit>,
235 <<J as Tensor>::Unit as UnitMul<<X as Tensor>::Unit>>::Output: UnitSum,
236 <<<J as Tensor>::Unit as UnitMul<<X as Tensor>::Unit>>::Output as UnitSum>::Output:
237 Is<<F as Tensor>::Unit>,
238 H: Hessian,
239 J: Jacobian,
240 for<'a> &'a J: Div<H, Output = X>,
241 J: Erase<Erased = E>,
242 X: Erase<Erased = E> + Solution,
243 E: Tensor,
244 <X as Tensor>::Unit: UnitDiv<<X as Tensor>::Unit, Output = Dimensionless>,
245 for<'a> &'a X: Mul<Quantity<Dimensionless>, Output = X> + Mul<Scalar, Output = X>,
246 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
247{
248 fn minimize_incremental(
249 &self,
250 mut function: impl FnMut(&X) -> Result<F, String>,
251 jacobian: impl FnMut(&X) -> Result<J, String>,
252 hessian: impl FnMut(&X) -> Result<H, String>,
253 update: impl FnMut(&X, &Vector, Scalar, bool) -> Result<(), String>,
254 initial_guess: X,
255 equality_constraint: EqualityConstraint,
256 sparse: Option<SparseSolver>,
257 ) -> Result<X, OptimizationError> {
258 let function = move |argument: &X| function(argument).map(|value| *value.erase());
259 match equality_constraint {
260 EqualityConstraint::Fixed(indices) => constrained_fixed(
261 self,
262 function,
263 jacobian,
264 hessian,
265 update,
266 initial_guess,
267 sparse,
268 indices,
269 ),
270 EqualityConstraint::Linear(constraint_matrix, constraint_rhs) => constrained(
271 self,
272 function,
273 jacobian,
274 hessian,
275 update,
276 initial_guess,
277 sparse,
278 constraint_matrix,
279 constraint_rhs,
280 ),
281 EqualityConstraint::None => unimplemented!(
282 "An unconstrained solution has no chained vector to lend the increment through."
283 ),
284 }
285 .map_err(|error| OptimizationError::upstream(error, self))
286 }
287}
288
289impl<U, V, Ru, Rv, Kuu, Kvu, Kuv, Kvv> FirstOrderRootFindingBlock<U, V, Ru, Rv, Kuu, Kvu, Kuv, Kvv>
290 for NewtonRaphson
291where
292 U: Solution,
293 V: Solution,
294 Ru: Jacobian,
295 Rv: Jacobian,
296 Kuu: HessianBlock,
297 Kvu: HessianBlock,
298 Kuv: HessianBlock,
299 Kvv: HessianBlock,
300 for<'a> &'a CscMatrix: Mul<&'a U, Output = Vector> + Mul<&'a V, Output = Vector>,
301{
302 fn root_block(
303 &self,
304 residual_global: impl FnMut(&U, &V) -> Result<Ru, String>,
305 residual_local: impl FnMut(&U, &V) -> Result<Rv, String>,
306 tangents: impl FnMut(&U, &V) -> Result<(Kuu, Kvu, Kuv, Kvv), String>,
307 initial_guess: (U, V),
308 constraint_global: (CscMatrix, Vector),
309 constraint_local: (CscMatrix, Vector),
310 sparse: Option<SparseSolver>,
311 strategy: SolveStrategy,
312 ) -> Result<(U, V), OptimizationError> {
313 blocked(
314 self,
315 |_: &U, _: &V| panic!("No line search in root finding"),
316 false,
317 residual_global,
318 residual_local,
319 tangents,
320 initial_guess,
321 constraint_global,
322 constraint_local,
323 sparse,
324 strategy,
325 )
326 .map_err(|error| OptimizationError::upstream(error, self))
327 }
328}
329
330impl<F, U, V, Ru, Rv, Kuu, Kvu, Kuv, Kvv>
331 SecondOrderOptimizationBlock<F, U, V, Ru, Rv, Kuu, Kvu, Kuv, Kvv> for NewtonRaphson
332where
333 F: Erase<Erased = Scalar> + Tensor,
334 <Ru as Tensor>::Unit: UnitMul<<U as Tensor>::Unit>,
335 <<Ru as Tensor>::Unit as UnitMul<<U as Tensor>::Unit>>::Output: UnitSum,
336 <<<Ru as Tensor>::Unit as UnitMul<<U as Tensor>::Unit>>::Output as UnitSum>::Output:
337 Is<<F as Tensor>::Unit>,
338 <Rv as Tensor>::Unit: UnitMul<<V as Tensor>::Unit>,
339 <<Rv as Tensor>::Unit as UnitMul<<V as Tensor>::Unit>>::Output: UnitSum,
340 <<<Rv as Tensor>::Unit as UnitMul<<V as Tensor>::Unit>>::Output as UnitSum>::Output:
341 Is<<F as Tensor>::Unit>,
342 U: Solution,
343 V: Solution,
344 Ru: Jacobian,
345 Rv: Jacobian,
346 Kuu: HessianBlock,
347 Kvu: HessianBlock,
348 Kuv: HessianBlock,
349 Kvv: HessianBlock,
350 for<'a> &'a CscMatrix: Mul<&'a U, Output = Vector> + Mul<&'a V, Output = Vector>,
351{
352 fn minimize_block(
353 &self,
354 mut function: impl FnMut(&U, &V) -> Result<F, String>,
355 residual_global: impl FnMut(&U, &V) -> Result<Ru, String>,
356 residual_local: impl FnMut(&U, &V) -> Result<Rv, String>,
357 tangents: impl FnMut(&U, &V) -> Result<(Kuu, Kvu, Kuv, Kvv), String>,
358 initial_guess: (U, V),
359 constraint_global: (CscMatrix, Vector),
360 constraint_local: (CscMatrix, Vector),
361 sparse: Option<SparseSolver>,
362 strategy: SolveStrategy,
363 ) -> Result<(U, V), OptimizationError> {
364 let function = move |global: &U, local: &V| function(global, local).map(|v| *v.erase());
365 blocked(
366 self,
367 function,
368 true,
369 residual_global,
370 residual_local,
371 tangents,
372 initial_guess,
373 constraint_global,
374 constraint_local,
375 sparse,
376 strategy,
377 )
378 .map_err(|error| OptimizationError::upstream(error, self))
379 }
380}
381
382const PENALTY_SAFETY: Scalar = 2.0;
383
384fn violation<M, T>(constraint_matrix: &M, constraint_rhs: &Vector, variables: &T) -> Scalar
385where
386 for<'a> &'a M: Mul<&'a T, Output = Vector>,
387{
388 (constraint_rhs - constraint_matrix * variables)
389 .iter()
390 .map(|entry| entry.abs())
391 .sum()
392}
393
394fn raise_penalty<'a>(
399 penalty: Scalar,
400 multipliers: impl Iterator<Item = (&'a Scalar, &'a Scalar)>,
401) -> Scalar {
402 penalty.max(
403 PENALTY_SAFETY
404 * multipliers.fold(0.0, |largest: Scalar, (multiplier, decrement)| {
405 largest.max((multiplier - decrement).abs())
406 }),
407 )
408}
409
410fn merit_slope<'a>(
416 gradients: impl Iterator<Item = (&'a Scalar, &'a Scalar)>,
417 violated: Scalar,
418) -> Scalar {
419 gradients
420 .map(|(gradient, decrement)| gradient * decrement)
421 .sum::<Scalar>()
422 + violated
423}
424
425fn backtrack_penalty(
439 newton_raphson: &NewtonRaphson,
440 merit: impl FnMut(Scalar) -> Result<Scalar, String>,
441 value: Scalar,
442 slope: Scalar,
443) -> Result<Scalar, OptimizationError> {
444 if slope <= Scalar::EPSILON * value.abs() {
445 Ok(1.0)
446 } else {
447 newton_raphson
448 .line_search
449 .backtrack_merit(merit, value, slope, 1.0)
450 .map_err(|error| OptimizationError::upstream(error, newton_raphson))
451 }
452}
453
454#[expect(clippy::too_many_arguments)]
455fn kkt_entry<Kuu, Kvu, Kuv, Kvv>(
456 row: usize,
457 column: usize,
458 num_global: usize,
459 num_outer: usize,
460 num_local: usize,
461 tangent_uu: &Kuu,
462 tangent_vu: &Kvu,
463 tangent_uv: &Kuv,
464 tangent_vv: &Kvv,
465 constraint_matrix_global: &CscMatrix,
466 constraint_matrix_local: &CscMatrix,
467) -> Scalar
468where
469 Kuu: HessianBlock,
470 Kvu: HessianBlock,
471 Kuv: HessianBlock,
472 Kvv: HessianBlock,
473{
474 let local = num_outer + num_local;
475 let row_global = row < num_global;
476 let row_local = (num_outer..local).contains(&row);
477 let column_global = column < num_global;
478 let column_local = (num_outer..local).contains(&column);
479 if row_global && column_global {
480 tangent_uu.entry(row, column)
481 } else if row_global && column_local {
482 tangent_uv.entry(row, column - num_outer)
483 } else if row_local && column_global {
484 tangent_vu.entry(row - num_outer, column)
485 } else if row_local && column_local {
486 tangent_vv.entry(row - num_outer, column - num_outer)
487 } else if row_global && (num_global..num_outer).contains(&column) {
488 -constraint_matrix_global.entry(column - num_global, row)
489 } else if (num_global..num_outer).contains(&row) && column_global {
490 -constraint_matrix_global.entry(row - num_global, column)
491 } else if row_local && column >= local {
492 -constraint_matrix_local.entry(column - local, row - num_outer)
493 } else if row >= local && column_local {
494 -constraint_matrix_local.entry(row - local, column - num_outer)
495 } else {
496 0.0
497 }
498}
499
500fn backtrack_errors(
509 newton_raphson: &NewtonRaphson,
510 mut reachable: impl FnMut(Scalar) -> bool,
511 cut_back: Scalar,
512 max_steps: usize,
513) -> Result<Scalar, OptimizationError> {
514 let mut trial_size = 1.0;
515 for _ in 0..max_steps {
516 if reachable(trial_size) {
517 return Ok(trial_size);
518 }
519 trial_size *= cut_back
520 }
521 Err(OptimizationError::upstream(
522 LineSearchError::MaximumStepsReached(
523 format!("{:?}", newton_raphson.line_search),
524 max_steps,
525 ),
526 newton_raphson,
527 ))
528}
529
530fn converged(
540 newton_raphson: &NewtonRaphson,
541 residual: &Vector,
542 variables: usize,
543 scales: &mut Option<(Scalar, Scalar)>,
544) -> bool {
545 let norms = (
546 newton_raphson
547 .error_norm
548 .over(residual.iter().take(variables).copied()),
549 newton_raphson
550 .error_norm
551 .over(residual.iter().skip(variables).copied()),
552 );
553 let scales = scales.get_or_insert(norms);
554 let met = |norm: Scalar, scale: Scalar, abs_tol: Scalar| {
555 norm < abs_tol
556 || newton_raphson
557 .rel_tol
558 .is_some_and(|rel_tol| norm / scale < rel_tol)
559 };
560 met(norms.0, scales.0, newton_raphson.abs_tol.residual)
561 && met(norms.1, scales.1, newton_raphson.abs_tol.constraint)
562}
563
564fn limit_decrement(newton_raphson: &NewtonRaphson, decrements: &mut [(&mut Vector, usize)]) {
569 if let TrustRegion::Fixed { radius, norm } = newton_raphson.trust_region {
570 let size = norm.over(
571 decrements
572 .iter()
573 .flat_map(|(decrement, variables)| decrement.iter().take(*variables).copied()),
574 );
575 if size > radius {
576 decrements
577 .iter_mut()
578 .for_each(|(decrement, _)| **decrement *= radius / size)
579 }
580 }
581}
582
583fn kkt_block<K>(
584 tangent: &K,
585 constraint_matrix: &CscMatrix,
586 size: usize,
587 block: &mut SquareMatrix,
588 offset: usize,
589) where
590 K: HessianBlock,
591{
592 tangent.fill_into_block(block, offset, offset);
593 constraint_matrix.iter().for_each(|(a, j, entry)| {
594 block[offset + size + a][offset + j] = -entry;
595 block[offset + j][offset + size + a] = -entry;
596 })
597}
598
599fn kkt_residual<R, T>(
600 residual: R,
601 multipliers: &Vector,
602 constraint_matrix: &CscMatrix,
603 constraint_rhs: &Vector,
604 variables: &T,
605 chained: &mut Vector,
606) where
607 R: Jacobian,
608 for<'a> &'a CscMatrix: Mul<&'a T, Output = Vector>,
609{
610 (residual - multipliers * constraint_matrix)
611 .fill_into_chained(constraint_rhs - constraint_matrix * variables, chained)
612}
613
614#[expect(clippy::too_many_arguments)]
620fn converge_local<U, V, Rv, Kuu, Kvu, Kuv, Kvv>(
621 local_solver: &NewtonRaphson,
622 residual_local: &mut impl FnMut(&U, &V) -> Result<Rv, String>,
623 tangents: &mut impl FnMut(&U, &V) -> Result<(Kuu, Kvu, Kuv, Kvv), String>,
624 global: &U,
625 local: &mut V,
626 multipliers_local: &mut Vector,
627 constraint_matrix_local: &CscMatrix,
628 constraint_rhs_local: &Vector,
629 num_local: usize,
630 update_inner: &mut Vector,
631 tangent_inner: &mut SquareMatrix,
632 factorization: &mut LuDecomposition,
633) -> Result<(), OptimizationError>
634where
635 Rv: Jacobian,
636 Kvv: HessianBlock,
637 V: Solution,
638 for<'a> &'a CscMatrix: Mul<&'a V, Output = Vector>,
639{
640 let mut local_steps = 0;
641 let mut scales = None;
642 loop {
643 kkt_residual(
644 residual_local(global, local)?,
645 multipliers_local,
646 constraint_matrix_local,
647 constraint_rhs_local,
648 local,
649 update_inner,
650 );
651 if converged(local_solver, update_inner, num_local, &mut scales)
652 || local_steps == local_solver.max_steps
653 {
654 return Ok(());
655 }
656 local_steps += 1;
657 let (_, _, _, tangent) = tangents(global, local)?;
658 kkt_block(
659 &tangent,
660 constraint_matrix_local,
661 num_local,
662 tangent_inner,
663 0,
664 );
665 tangent_inner.factorize_lu_into(factorization)?;
666 let mut decrement = factorization.solve(update_inner);
667 limit_decrement(local_solver, &mut [(&mut decrement, num_local)]);
668 local.decrement_from_chained(multipliers_local, &decrement)
669 }
670}
671
672#[expect(clippy::too_many_arguments)]
673fn blocked<U, V, Ru, Rv, Kuu, Kvu, Kuv, Kvv>(
674 newton_raphson: &NewtonRaphson,
675 mut function: impl FnMut(&U, &V) -> Result<Scalar, String>,
676 minimizing: bool,
677 mut residual_global: impl FnMut(&U, &V) -> Result<Ru, String>,
678 mut residual_local: impl FnMut(&U, &V) -> Result<Rv, String>,
679 mut tangents: impl FnMut(&U, &V) -> Result<(Kuu, Kvu, Kuv, Kvv), String>,
680 initial_guess: (U, V),
681 constraint_global: (CscMatrix, Vector),
682 constraint_local: (CscMatrix, Vector),
683 sparse: Option<SparseSolver>,
684 strategy: SolveStrategy,
685) -> Result<(U, V), OptimizationError>
686where
687 U: Solution,
688 V: Solution,
689 Ru: Jacobian,
690 Rv: Jacobian,
691 Kuu: HessianBlock,
692 Kvu: HessianBlock,
693 Kuv: HessianBlock,
694 Kvv: HessianBlock,
695 for<'a> &'a CscMatrix: Mul<&'a U, Output = Vector> + Mul<&'a V, Output = Vector>,
696{
697 let (mut global, mut local) = initial_guess;
698 let mut penalty = 0.0 as Scalar;
699 let (constraint_matrix_global, constraint_rhs_global) = constraint_global;
700 let (constraint_matrix_local, constraint_rhs_local) = constraint_local;
701 let num_global = global.size();
702 let num_local = local.size();
703 let num_outer = num_global + constraint_rhs_global.len();
704 let num_inner = num_local + constraint_rhs_local.len();
705 let mut multipliers_global = Vector::zero(constraint_rhs_global.len());
706 let mut multipliers_local = Vector::zero(constraint_rhs_local.len());
707 let eliminating = !matches!(strategy, SolveStrategy::Monolithic { elimination: false });
708 let condensed = match strategy {
709 SolveStrategy::Condensed(ref local_solver) => Some(local_solver),
710 SolveStrategy::Monolithic { .. } => None,
711 };
712 if sparse.is_some() && eliminating {
713 unimplemented!(
714 "Eliminating the local block sparsely wants it held as the blocks it is, not as one matrix."
715 )
716 }
717 let (inner, outer) = if eliminating {
718 (num_inner, num_outer)
719 } else {
720 (0, 0)
721 };
722 let whole = if eliminating || sparse.is_some() {
723 0
724 } else {
725 num_outer + num_inner
726 };
727 let mut column = Vector::zero(inner);
728 let mut coupling_global = Matrix::zero(outer, inner);
729 let mut coupling_local = Matrix::zero(inner, outer);
730 let mut decrement_inner = Vector::zero(num_inner);
731 let mut decrement_outer = Vector::zero(num_outer);
732 let mut decrement_whole = Vector::zero(whole);
733 let mut eliminated = vec![Vector::zero(inner); outer.min(num_global)];
734 let mut factorization = LuDecomposition::zero(inner);
735 let mut factorization_outer = LuDecomposition::zero(outer);
736 let mut factorization_whole = LuDecomposition::zero(whole);
737 let mut monolithic = SquareMatrix::zero(whole);
738 let mut residual = Vector::zero(num_outer + num_inner);
739 let mut scales_inner = None;
740 let mut scales_outer = None;
741 let mut tangent_inner = SquareMatrix::zero(inner);
742 let mut tangent_outer = SquareMatrix::zero(outer);
743 let mut update_inner = Vector::zero(num_inner);
744 let mut update_outer = Vector::zero(num_outer);
745 let mut steps = 0;
746 loop {
747 if let Some(local_solver) = condensed {
748 converge_local(
749 local_solver,
750 &mut residual_local,
751 &mut tangents,
752 &global,
753 &mut local,
754 &mut multipliers_local,
755 &constraint_matrix_local,
756 &constraint_rhs_local,
757 num_local,
758 &mut update_inner,
759 &mut tangent_inner,
760 &mut factorization,
761 )?
762 }
763 kkt_residual(
764 residual_global(&global, &local)?,
765 &multipliers_global,
766 &constraint_matrix_global,
767 &constraint_rhs_global,
768 &global,
769 &mut update_outer,
770 );
771 kkt_residual(
772 residual_local(&global, &local)?,
773 &multipliers_local,
774 &constraint_matrix_local,
775 &constraint_rhs_local,
776 &local,
777 &mut update_inner,
778 );
779 update_outer
780 .iter()
781 .chain(update_inner.iter())
782 .zip(residual.iter_mut())
783 .for_each(|(entry, residual_i)| *residual_i = *entry);
784 let converged_outer =
785 converged(newton_raphson, &update_outer, num_global, &mut scales_outer);
786 let converged_inner =
787 converged(newton_raphson, &update_inner, num_local, &mut scales_inner);
788 if converged_outer && converged_inner {
789 return Ok((global, local));
790 } else if steps == newton_raphson.max_steps {
791 return Err(OptimizationError::MaximumStepsReached(
792 newton_raphson.max_steps,
793 format!("{newton_raphson:?}"),
794 ));
795 }
796 steps += 1;
797 let (tangent_uu, tangent_vu, tangent_uv, tangent_vv) = tangents(&global, &local)?;
798 if eliminating {
799 kkt_block(
800 &tangent_uu,
801 &constraint_matrix_global,
802 num_global,
803 &mut tangent_outer,
804 0,
805 );
806 kkt_block(
807 &tangent_vv,
808 &constraint_matrix_local,
809 num_local,
810 &mut tangent_inner,
811 0,
812 );
813 tangent_uv.fill_into_block(&mut coupling_global, 0, 0);
814 tangent_vu.fill_into_block(&mut coupling_local, 0, 0);
815 tangent_inner.factorize_lu_into(&mut factorization)?;
816 eliminated
817 .iter_mut()
818 .enumerate()
819 .for_each(|(k, eliminated_k)| {
820 (0..num_local).for_each(|i| column[i] = coupling_local[i][k]);
821 factorization.solve_into(&column, eliminated_k)
822 });
823 factorization.solve_into(&update_inner, &mut decrement_inner);
824 (0..num_global).for_each(|i| {
825 (0..num_local).for_each(|j| {
826 let coupling = coupling_global[i][j];
827 (0..num_global)
828 .for_each(|k| tangent_outer[i][k] -= coupling * eliminated[k][j]);
829 update_outer[i] -= coupling * decrement_inner[j]
830 })
831 });
832 tangent_outer.factorize_lu_into(&mut factorization_outer)?;
833 factorization_outer.solve_into(&update_outer, &mut decrement_outer);
834 (0..num_global).for_each(|k| {
835 decrement_inner
836 .iter_mut()
837 .zip(eliminated[k].iter())
838 .for_each(|(decrement_inner_i, eliminated_ki)| {
839 *decrement_inner_i -= eliminated_ki * decrement_outer[k]
840 })
841 });
842 } else {
843 if sparse.is_none() {
844 kkt_block(
845 &tangent_uu,
846 &constraint_matrix_global,
847 num_global,
848 &mut monolithic,
849 0,
850 );
851 kkt_block(
852 &tangent_vv,
853 &constraint_matrix_local,
854 num_local,
855 &mut monolithic,
856 num_outer,
857 );
858 }
859 if let Some(ref solver) = sparse {
860 decrement_whole = solver.solve(
865 |i, j| {
866 kkt_entry(
867 i,
868 j,
869 num_global,
870 num_outer,
871 num_local,
872 &tangent_uu,
873 &tangent_vu,
874 &tangent_uv,
875 &tangent_vv,
876 &constraint_matrix_global,
877 &constraint_matrix_local,
878 )
879 },
880 &residual,
881 )?
882 } else {
883 tangent_uv.fill_into_block(&mut monolithic, 0, num_outer);
884 tangent_vu.fill_into_block(&mut monolithic, num_outer, 0);
885 monolithic.factorize_lu_into(&mut factorization_whole)?;
886 factorization_whole.solve_into(&residual, &mut decrement_whole)
887 }
888 decrement_whole
889 .iter()
890 .zip(decrement_outer.iter_mut().chain(decrement_inner.iter_mut()))
891 .for_each(|(entry, decrement_i)| *decrement_i = *entry);
892 }
893 limit_decrement(
894 newton_raphson,
895 &mut [
896 (&mut decrement_outer, num_global),
897 (&mut decrement_inner, num_local),
898 ],
899 );
900 let step_size = if matches!(newton_raphson.line_search, LineSearch::None) {
901 1.0
902 } else if !minimizing
903 && let LineSearch::Error {
904 cut_back,
905 max_steps,
906 } = &newton_raphson.line_search
907 {
908 backtrack_errors(
914 newton_raphson,
915 |trial_size| {
916 let mut trial_global = global.clone();
917 let mut trial_local = local.clone();
918 let mut trial_multipliers_global = multipliers_global.clone();
919 let mut trial_multipliers_local = multipliers_local.clone();
920 trial_global.decrement_from_chained(
921 &mut trial_multipliers_global,
922 &(&decrement_outer * trial_size),
923 );
924 let reached = if let Some(local_solver) = condensed {
925 converge_local(
926 local_solver,
927 &mut residual_local,
928 &mut tangents,
929 &trial_global,
930 &mut trial_local,
931 &mut trial_multipliers_local,
932 &constraint_matrix_local,
933 &constraint_rhs_local,
934 num_local,
935 &mut update_inner,
936 &mut tangent_inner,
937 &mut factorization,
938 )
939 .is_ok()
940 } else {
941 trial_local.decrement_from_chained(
942 &mut trial_multipliers_local,
943 &(&decrement_inner * trial_size),
944 );
945 true
946 };
947 reached
948 && residual_global(&trial_global, &trial_local).is_ok()
949 && residual_local(&trial_global, &trial_local).is_ok()
950 && tangents(&trial_global, &trial_local).is_ok()
951 },
952 *cut_back,
953 *max_steps,
954 )?
955 } else {
956 penalty = raise_penalty(
957 penalty,
958 multipliers_global
959 .iter()
960 .zip(decrement_outer.iter().skip(num_global))
961 .chain(
962 multipliers_local
963 .iter()
964 .zip(decrement_inner.iter().skip(num_local)),
965 ),
966 );
967 let violated = penalty
968 * (violation(&constraint_matrix_global, &constraint_rhs_global, &global)
969 + violation(&constraint_matrix_local, &constraint_rhs_local, &local));
970 let mut gradient_global = Vector::zero(num_global);
971 let mut gradient_local = Vector::zero(num_local);
972 residual_global(&global, &local)?.fill_into(&mut gradient_global);
973 residual_local(&global, &local)?.fill_into(&mut gradient_local);
974 let slope = merit_slope(
975 gradient_global
976 .iter()
977 .zip(decrement_outer.iter())
978 .chain(gradient_local.iter().zip(decrement_inner.iter())),
979 violated,
980 );
981 let value = function(&global, &local)? + violated;
982 backtrack_penalty(
983 newton_raphson,
984 |step| {
985 let mut trial_global = global.clone();
986 let mut trial_local = local.clone();
987 let mut trial_multipliers_global = multipliers_global.clone();
988 let mut trial_multipliers_local = multipliers_local.clone();
989 trial_global.decrement_from_chained(
990 &mut trial_multipliers_global,
991 &(&decrement_outer * step),
992 );
993 if let Some(local_solver) = condensed {
999 converge_local(
1000 local_solver,
1001 &mut residual_local,
1002 &mut tangents,
1003 &trial_global,
1004 &mut trial_local,
1005 &mut trial_multipliers_local,
1006 &constraint_matrix_local,
1007 &constraint_rhs_local,
1008 num_local,
1009 &mut update_inner,
1010 &mut tangent_inner,
1011 &mut factorization,
1012 )
1013 .map_err(|error| format!("{error}"))?
1014 } else {
1015 trial_local.decrement_from_chained(
1016 &mut trial_multipliers_local,
1017 &(&decrement_inner * step),
1018 )
1019 }
1020 Ok(function(&trial_global, &trial_local)?
1021 + penalty
1022 * (violation(
1023 &constraint_matrix_global,
1024 &constraint_rhs_global,
1025 &trial_global,
1026 ) + violation(
1027 &constraint_matrix_local,
1028 &constraint_rhs_local,
1029 &trial_local,
1030 )))
1031 },
1032 value,
1033 slope,
1034 )?
1035 };
1036 if step_size == 1.0 {
1037 global.decrement_from_chained(&mut multipliers_global, &decrement_outer);
1038 local.decrement_from_chained(&mut multipliers_local, &decrement_inner)
1039 } else {
1040 global.decrement_from_chained(&mut multipliers_global, &(&decrement_outer * step_size));
1041 local.decrement_from_chained(&mut multipliers_local, &(&decrement_inner * step_size))
1042 }
1043 }
1044}
1045
1046fn unconstrained<J, H, X, E>(
1047 newton_raphson: &NewtonRaphson,
1048 mut function: impl FnMut(&X) -> Result<Scalar, String>,
1049 mut jacobian: impl FnMut(&X) -> Result<J, String>,
1050 mut hessian: impl FnMut(&X) -> Result<H, String>,
1051 initial_guess: X,
1052 sparse: Option<SparseSolver>,
1053) -> Result<X, OptimizationError>
1054where
1055 H: Hessian,
1056 J: Jacobian,
1057 for<'a> &'a J: Div<H, Output = X>,
1058 J: Erase<Erased = E>,
1059 X: Erase<Erased = E> + Solution,
1060 E: Tensor,
1061 <X as Tensor>::Unit: UnitDiv<<X as Tensor>::Unit, Output = Dimensionless>,
1062 for<'a> &'a X: Mul<Quantity<Dimensionless>, Output = X> + Mul<Scalar, Output = X>,
1063{
1064 let mut decrement;
1065 let mut flattened = Vector::zero(if sparse.is_none() {
1066 0
1067 } else {
1068 initial_guess.size()
1069 });
1070 let mut residual;
1071 let mut solution = initial_guess;
1072 let mut step_size;
1073 let mut steps = 0;
1074 loop {
1075 residual = jacobian(&solution)?;
1076 if newton_raphson.error_norm.apply(&residual) < newton_raphson.abs_tol.residual() {
1077 return Ok(solution);
1078 } else if steps == newton_raphson.max_steps {
1079 return Err(OptimizationError::MaximumStepsReached(
1080 newton_raphson.max_steps,
1081 format!("{newton_raphson:?}"),
1082 ));
1083 } else {
1084 steps += 1;
1085 decrement = if let Some(ref solver) = sparse {
1086 let hess = hessian(&solution)?;
1087 residual.fill_into(&mut flattened);
1088 X::from(solver.solve(|i, j| hess.entry(i, j), &flattened)?)
1089 } else {
1090 &residual / hessian(&solution)?
1091 };
1092 if let TrustRegion::Fixed { radius, norm } = newton_raphson.trust_region {
1093 let size = norm.measure(&decrement);
1094 if size > radius {
1095 decrement *= radius / size
1096 }
1097 }
1098 step_size = newton_raphson.backtracking_line_search::<X, E>(
1099 |trial: &X, _: Scalar| function(trial),
1100 &mut jacobian,
1101 &solution,
1102 &residual,
1103 &decrement,
1104 1.0,
1105 )?;
1106 if step_size != 1.0 {
1107 decrement *= step_size
1108 }
1109 solution -= decrement
1110 }
1111 }
1112}
1113
1114#[expect(clippy::too_many_arguments)]
1115fn constrained_fixed<J, H, X, E>(
1116 newton_raphson: &NewtonRaphson,
1117 mut function: impl FnMut(&X) -> Result<Scalar, String>,
1118 mut jacobian: impl FnMut(&X) -> Result<J, String>,
1119 mut hessian: impl FnMut(&X) -> Result<H, String>,
1120 mut update: impl FnMut(&X, &Vector, Scalar, bool) -> Result<(), String>,
1121 initial_guess: X,
1122 sparse: Option<SparseSolver>,
1123 indices: Vec<usize>,
1124) -> Result<X, OptimizationError>
1125where
1126 H: Hessian,
1127 J: Jacobian,
1128 J: Erase<Erased = E>,
1129 X: Erase<Erased = E> + Solution,
1130 E: Tensor,
1131 <X as Tensor>::Unit: UnitDiv<<X as Tensor>::Unit, Output = Dimensionless>,
1132 for<'a> &'a X: Mul<Quantity<Dimensionless>, Output = X> + Mul<Scalar, Output = X>,
1133{
1134 let mut applied = Vector::zero(initial_guess.size());
1135 let mut retained = vec![true; initial_guess.size()];
1136 indices.iter().for_each(|&index| retained[index] = false);
1137 let unmap: Vec<usize> = retained
1138 .iter()
1139 .enumerate()
1140 .filter_map(|(index, &keep)| keep.then_some(index))
1141 .collect();
1142 let mut decrement = Vector::zero(unmap.len());
1143 let mut factorization = LuDecomposition::zero(if sparse.is_none() { unmap.len() } else { 0 });
1144 let mut residual;
1145 let mut solution = initial_guess;
1146 let mut step_size;
1147 let mut steps = 0;
1148 loop {
1149 residual = jacobian(&solution)?.retain_from(&retained);
1150 if newton_raphson.error_norm.apply(&residual) < newton_raphson.abs_tol.residual() {
1151 return Ok(solution);
1152 } else if steps == newton_raphson.max_steps {
1153 return Err(OptimizationError::MaximumStepsReached(
1154 newton_raphson.max_steps,
1155 format!("{newton_raphson:?}"),
1156 ));
1157 } else if let Some(ref solver) = sparse {
1158 let hess = hessian(&solution)?;
1159 decrement = solver.solve(|i, j| hess.entry(unmap[i], unmap[j]), &residual)?
1160 } else {
1161 hessian(&solution)?
1162 .retain_from(&retained)
1163 .factorize_lu_into(&mut factorization)?;
1164 factorization.solve_into(&residual, &mut decrement)
1165 }
1166 steps += 1;
1167 limit_decrement(newton_raphson, &mut [(&mut decrement, unmap.len())]);
1168 applied.iter_mut().for_each(|entry| *entry = 0.0);
1174 unmap
1175 .iter()
1176 .zip(decrement.iter())
1177 .for_each(|(&index, decrement_a)| applied[index] = *decrement_a);
1178 step_size = if matches!(newton_raphson.line_search, LineSearch::None) {
1179 1.0
1180 } else if let LineSearch::Error {
1181 cut_back,
1182 max_steps,
1183 } = &newton_raphson.line_search
1184 {
1185 backtrack_errors(
1186 newton_raphson,
1187 |trial_size| {
1188 let mut trial = solution.clone();
1189 trial.decrement_from_retained(&retained, &(&decrement * trial_size));
1190 update(&solution, &applied, trial_size, false).is_ok()
1191 && jacobian(&trial).is_ok()
1192 },
1193 *cut_back,
1194 *max_steps,
1195 )?
1196 } else {
1197 let jac = jacobian(&solution)?;
1198 let mut decrement_full = &solution * 0.0;
1199 decrement_full.decrement_from_retained(&retained, &decrement);
1200 decrement_full *= -1.0;
1201 newton_raphson.backtracking_line_search::<X, E>(
1202 |trial: &X, step: Scalar| {
1203 update(&solution, &applied, step, false)?;
1204 function(trial)
1205 },
1206 &mut jacobian,
1207 &solution,
1208 &jac,
1209 &decrement_full,
1210 1.0,
1211 )?
1212 };
1213 update(&solution, &applied, step_size, true)?;
1214 if step_size != 1.0 {
1215 decrement *= step_size
1216 }
1217 solution.decrement_from_retained(&retained, &decrement)
1218 }
1219}
1220
1221#[expect(clippy::too_many_arguments)]
1222fn constrained<J, H, X>(
1223 newton_raphson: &NewtonRaphson,
1224 mut function: impl FnMut(&X) -> Result<Scalar, String>,
1225 mut jacobian: impl FnMut(&X) -> Result<J, String>,
1226 mut hessian: impl FnMut(&X) -> Result<H, String>,
1227 mut update: impl FnMut(&X, &Vector, Scalar, bool) -> Result<(), String>,
1228 initial_guess: X,
1229 sparse: Option<SparseSolver>,
1230 constraint_matrix: Matrix,
1231 constraint_rhs: Vector,
1232) -> Result<X, OptimizationError>
1233where
1234 H: Hessian,
1235 J: Jacobian,
1236 X: Solution,
1237 for<'a> &'a Matrix: Mul<&'a X, Output = Vector>,
1238{
1239 let mut penalty = 0.0 as Scalar;
1240 let num_variables = initial_guess.size();
1241 let mut applied = Vector::zero(num_variables);
1242 let num_constraints = constraint_rhs.len();
1243 let num_total = num_variables + num_constraints;
1244 let mut decrement = Vector::zero(num_total);
1245 let mut factorization = LuDecomposition::zero(if sparse.is_none() { num_total } else { 0 });
1246 let mut multipliers = Vector::zero(num_constraints);
1247 let mut residual = Vector::zero(num_total);
1248 let mut scales = None;
1249 let mut solution = initial_guess;
1250 let mut tangent = SquareMatrix::zero(if sparse.is_none() { num_total } else { 0 });
1251 if sparse.is_none() {
1252 constraint_matrix
1253 .iter()
1254 .enumerate()
1255 .for_each(|(i, constraint_matrix_i)| {
1256 constraint_matrix_i
1257 .iter()
1258 .enumerate()
1259 .for_each(|(j, constraint_matrix_ij)| {
1260 tangent[i + num_variables][j] = -constraint_matrix_ij;
1261 tangent[j][i + num_variables] = -constraint_matrix_ij;
1262 })
1263 });
1264 }
1265 let mut steps = 0;
1266 loop {
1267 (jacobian(&solution)? - &multipliers * &constraint_matrix).fill_into_chained(
1268 &constraint_rhs - &constraint_matrix * &solution,
1269 &mut residual,
1270 );
1271 if converged(newton_raphson, &residual, num_variables, &mut scales) {
1272 return Ok(solution);
1273 } else if steps == newton_raphson.max_steps {
1274 return Err(OptimizationError::MaximumStepsReached(
1275 newton_raphson.max_steps,
1276 format!("{newton_raphson:?}"),
1277 ));
1278 } else if let Some(ref solver) = sparse {
1279 let hess = hessian(&solution)?;
1280 decrement = solver.solve(
1281 |i, j| {
1282 if i >= num_variables {
1283 -constraint_matrix[i - num_variables][j]
1284 } else if j >= num_variables {
1285 -constraint_matrix[j - num_variables][i]
1286 } else {
1287 hess.entry(i, j)
1288 }
1289 },
1290 &residual,
1291 )?;
1292 } else {
1293 hessian(&solution)?.fill_into(&mut tangent);
1294 tangent.factorize_lu_into(&mut factorization)?;
1295 factorization.solve_into(&residual, &mut decrement)
1296 }
1297 steps += 1;
1298 limit_decrement(newton_raphson, &mut [(&mut decrement, num_variables)]);
1299 applied
1304 .iter_mut()
1305 .zip(decrement.iter())
1306 .for_each(|(applied_i, decrement_i)| *applied_i = *decrement_i);
1307 let step_size = if matches!(newton_raphson.line_search, LineSearch::None) {
1308 1.0
1309 } else if let LineSearch::Error {
1310 cut_back,
1311 max_steps,
1312 } = &newton_raphson.line_search
1313 {
1314 backtrack_errors(
1315 newton_raphson,
1316 |trial_size| {
1317 let mut trial = solution.clone();
1318 let mut trial_multipliers = multipliers.clone();
1319 trial
1320 .decrement_from_chained(&mut trial_multipliers, &(&decrement * trial_size));
1321 update(&solution, &applied, trial_size, false).is_ok()
1322 && jacobian(&trial).is_ok()
1323 },
1324 *cut_back,
1325 *max_steps,
1326 )?
1327 } else {
1328 penalty = raise_penalty(
1329 penalty,
1330 multipliers.iter().zip(decrement.iter().skip(num_variables)),
1331 );
1332 let violated = penalty * violation(&constraint_matrix, &constraint_rhs, &solution);
1333 let mut gradient = Vector::zero(num_variables);
1334 jacobian(&solution)?.fill_into(&mut gradient);
1335 let slope = merit_slope(gradient.iter().zip(decrement.iter()), violated);
1336 update(&solution, &applied, 0.0, false)?;
1337 let value = function(&solution)? + violated;
1338 backtrack_penalty(
1339 newton_raphson,
1340 |step| {
1341 let mut trial = solution.clone();
1342 let mut trial_multipliers = multipliers.clone();
1343 trial.decrement_from_chained(&mut trial_multipliers, &(&decrement * step));
1344 update(&solution, &applied, step, false)?;
1345 Ok(function(&trial)?
1346 + penalty * violation(&constraint_matrix, &constraint_rhs, &trial))
1347 },
1348 value,
1349 slope,
1350 )?
1351 };
1352 update(&solution, &applied, step_size, true)?;
1358 if step_size != 1.0 {
1359 decrement *= step_size
1360 }
1361 solution.decrement_from_chained(&mut multipliers, &decrement)
1362 }
1363}