1use super::{
6 DspError, LeastSquaresResult, Mat, QuadraticModel, SelectTrace, dot, feasible, grad, norm,
7 scaling, select, termination, trust,
8};
9use faer::{
10 Par,
11 diag::Diag,
12 dyn_stack::{MemBuffer, MemStack},
13 linalg::evd::{self, ComputeEigenvectors},
14};
15
16pub trait NormalEquations {
18 fn residual(&mut self, x: &[f64]) -> Vec<f64>;
20 fn normal(&mut self, x: &[f64], f: &[f64]) -> Result<(Mat<f64>, Vec<f64>), DspError>;
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum XScale {
27 Unit,
28 Jac,
29}
30
31#[derive(Debug, Clone, Copy)]
32pub struct NormalOptions {
33 pub max_nfev: usize,
34 pub x_scale: XScale,
35 pub ftol: f64,
36 pub xtol: f64,
37 pub gtol: f64,
38}
39impl Default for NormalOptions {
40 fn default() -> Self {
41 Self {
42 max_nfev: 100,
43 x_scale: XScale::Unit,
44 ftol: 1e-8,
45 xtol: 1e-8,
46 gtol: 1e-8,
47 }
48 }
49}
50
51#[derive(Debug, Default)]
53pub struct NormalTrial {
54 pub x: Vec<f64>,
55 pub cost: f64,
56 pub scale_inv: Vec<f64>,
57 pub v: Vec<f64>,
58 pub delta: f64,
59 pub alpha_in: f64,
60 pub alpha_out: f64,
62 pub s: Vec<f64>,
63 pub full_rank: bool,
64 pub step: Vec<f64>,
65 pub x_new: Vec<f64>,
66 pub predicted_reduction: f64,
67 pub actual_reduction: f64,
68 pub delta_new: f64,
69 pub ratio: f64,
70 pub accepted: bool,
71 pub select: SelectTrace,
72}
73#[derive(Debug, Default)]
74pub struct NormalTrace {
75 pub trials: Vec<NormalTrial>,
76}
77
78pub fn least_squares_normal<P: NormalEquations>(
82 problem: &mut P,
83 x0: &[f64],
84 lb: &[f64],
85 ub: &[f64],
86 options: &NormalOptions,
87) -> Result<LeastSquaresResult, DspError> {
88 least_squares_normal_with_trace(problem, x0, lb, ub, options, None)
89}
90
91pub fn least_squares_normal_with_trace<P: NormalEquations>(
92 problem: &mut P,
93 x0: &[f64],
94 lb: &[f64],
95 ub: &[f64],
96 options: &NormalOptions,
97 trace: Option<&mut NormalTrace>,
98) -> Result<LeastSquaresResult, DspError> {
99 solve(problem, x0, lb, ub, options, trace, None)
100}
101
102#[doc(hidden)]
105#[allow(clippy::too_many_arguments)]
106pub fn trf_normal_one_step<P: NormalEquations>(
107 problem: &mut P,
108 x: &[f64],
109 lb: &[f64],
110 ub: &[f64],
111 delta: f64,
112 alpha: f64,
113 scale_inv: &[f64],
114) -> Result<NormalTrial, DspError> {
115 if !delta.is_finite()
116 || delta <= 0.
117 || !alpha.is_finite()
118 || alpha < 0.
119 || scale_inv.len() != x.len()
120 || scale_inv.iter().any(|s| !s.is_finite() || *s <= 0.)
121 {
122 return Err(crate::invalid("invalid normal TRF replay state"));
123 }
124 let mut trace = NormalTrace::default();
125 solve(
126 problem,
127 x,
128 lb,
129 ub,
130 &NormalOptions {
131 max_nfev: 2,
132 gtol: 0.,
133 ..NormalOptions::default()
134 },
135 Some(&mut trace),
136 Some((delta, alpha, scale_inv)),
137 )?;
138 trace
139 .trials
140 .pop()
141 .ok_or_else(|| crate::invalid("no normal TRF replay trial"))
142}
143
144fn checked_normal<P: NormalEquations>(
145 problem: &mut P,
146 x: &[f64],
147 f: &[f64],
148) -> Result<(Mat<f64>, Vec<f64>), DspError> {
149 let (h, g) = problem.normal(x, f)?;
150 let n = x.len();
151 if h.nrows() != n
152 || h.ncols() != n
153 || g.len() != n
154 || g.iter().any(|v| !v.is_finite())
155 || (0..n).any(|i| h[(i, i)] < 0. || (0..n).any(|j| !h[(i, j)].is_finite()))
156 {
157 return Err(crate::invalid("invalid/nonfinite TRF normal equations"));
158 }
159 Ok((h, g))
160}
161
162fn jac_scale(h: &Mat<f64>, old: Option<&[f64]>) -> Vec<f64> {
163 (0..h.ncols())
166 .map(|i| {
167 let s = h[(i, i)].sqrt();
168 match old {
169 Some(old) => s.max(old[i]),
170 None if s == 0. => 1.,
171 None => s,
172 }
173 })
174 .collect()
175}
176
177struct Model {
178 h: Mat<f64>,
179 diag: Vec<f64>,
180 gh: Vec<f64>,
181 d: Vec<f64>,
182 s: Vec<f64>,
183 vectors: Mat<f64>,
184 uf: Vec<f64>,
185}
186impl Model {
187 fn new(
188 h: &Mat<f64>,
189 g: &[f64],
190 v: &[f64],
191 dv: &[f64],
192 scale_inv: &[f64],
193 ) -> Result<Self, DspError> {
194 let n = g.len();
195 let scale: Vec<_> = scale_inv.iter().map(|s| 1. / s).collect();
197 let d: Vec<_> = (0..n).map(|i| v[i].sqrt() * scale[i]).collect();
198 let diag: Vec<_> = (0..n).map(|i| g[i] * dv[i] * scale[i]).collect();
199 let gh: Vec<_> = (0..n).map(|i| d[i] * g[i]).collect();
200 let h = Mat::from_fn(n, n, |i, j| d[i] * h[(i, j)] * d[j]);
202 let augmented = Mat::from_fn(n, n, |i, j| h[(i, j)] + if i == j { diag[i] } else { 0. });
203 if (0..n).any(|i| (0..n).any(|j| !augmented[(i, j)].is_finite())) {
204 return Err(crate::invalid("nonfinite TRF eigenproblem"));
205 }
206 let mut eigenvalues = Diag::zeros(n);
207 let mut vectors = Mat::zeros(n, n);
208 let mut scratch = MemBuffer::new(evd::self_adjoint_evd_scratch::<f64>(
209 n,
210 ComputeEigenvectors::Yes,
211 Par::Seq,
212 Default::default(),
213 ));
214 evd::self_adjoint_evd(
215 augmented.as_ref(),
216 eigenvalues.as_mut(),
217 Some(vectors.as_mut()),
218 Par::Seq,
219 MemStack::new(&mut scratch),
220 Default::default(),
221 )
222 .map_err(|_| crate::invalid("TRF eigendecomposition did not converge"))?;
223 if (0..n).any(|i| {
224 !eigenvalues.column_vector()[i].is_finite()
225 || (0..n).any(|j| !vectors[(i, j)].is_finite())
226 }) {
227 return Err(crate::invalid("nonfinite TRF eigendecomposition"));
228 }
229 let s: Vec<_> = (0..n)
231 .rev()
232 .map(|i| eigenvalues.column_vector()[i].max(0.).sqrt())
233 .collect();
234 let vectors = Mat::from_fn(n, n, |i, j| vectors[(i, n - 1 - j)]);
235 let suf = grad(&vectors, &gh);
236 let uf = suf
237 .iter()
238 .zip(&s)
239 .map(|(v, s)| if *s > 0. { v / s } else { 0. })
240 .collect();
241 Ok(Self {
244 h,
245 diag,
246 gh,
247 d,
248 s,
249 vectors,
250 uf,
251 })
252 }
253}
254
255#[allow(clippy::too_many_arguments)]
256fn solve<P: NormalEquations>(
257 problem: &mut P,
258 x0: &[f64],
259 lb: &[f64],
260 ub: &[f64],
261 options: &NormalOptions,
262 mut trace: Option<&mut NormalTrace>,
263 replay: Option<(f64, f64, &[f64])>,
264) -> Result<LeastSquaresResult, DspError> {
265 let n = x0.len();
266 let tols = [options.ftol, options.xtol, options.gtol];
267 if n == 0
268 || lb.len() != n
269 || ub.len() != n
270 || options.max_nfev == 0
271 || tols.iter().any(|t| !t.is_finite() || *t < 0.)
272 || tols.iter().all(|t| *t <= f64::EPSILON)
273 || !lb.iter().chain(ub).any(|b| b.is_finite())
274 || (0..n).any(|i| {
275 !x0[i].is_finite()
276 || lb[i].is_nan()
277 || ub[i].is_nan()
278 || lb[i] >= ub[i]
279 || x0[i] < lb[i]
280 || x0[i] > ub[i]
281 })
282 {
283 return Err(crate::invalid("invalid normal TRF start/bounds/options"));
284 }
285 if let Some(t) = trace.as_deref_mut() {
286 t.trials.clear();
287 }
288 let mut x = x0.to_vec();
289 if replay.is_none() {
290 feasible(&mut x, lb, ub, true);
291 }
292 let mut f = problem.residual(&x);
293 let m = f.len();
294 if m == 0 || f.iter().any(|v| !v.is_finite()) {
295 return Err(crate::invalid("invalid initial residual"));
296 }
297 let (mut h, mut g) = checked_normal(problem, &x, &f)?;
298 let mut scale_inv = match options.x_scale {
299 XScale::Unit => vec![1.; n],
300 XScale::Jac => jac_scale(&h, None),
301 };
302 let (mut v, dv) = scaling(&x, &g, lb, ub);
303 for i in 0..n {
304 if dv[i] != 0. {
305 v[i] *= scale_inv[i];
306 }
307 }
308 let mut delta = norm(
310 &(0..n)
311 .map(|i| x[i] * scale_inv[i] / v[i].sqrt())
312 .collect::<Vec<_>>(),
313 );
314 if delta == 0. {
315 delta = 1.;
316 }
317 let mut alpha = 0.;
318 if let Some((dd, aa, ss)) = replay {
319 delta = dd;
320 alpha = aa;
321 scale_inv = ss.to_vec();
322 }
323 let mut cost = 0.5 * dot(&f, &f);
324 let (mut nfev, mut njev, mut status) = (1, 1, 0);
325 let optimality = loop {
326 let (mut v, dv) = scaling(&x, &g, lb, ub);
327 let gn = g
328 .iter()
329 .zip(&v)
330 .map(|(g, v)| (g * v).abs())
331 .fold(0., f64::max);
332 if gn < options.gtol {
333 status = 1;
334 }
335 if status != 0 || nfev == options.max_nfev {
336 break gn;
337 }
338 for i in 0..n {
340 if dv[i] != 0. {
341 v[i] *= scale_inv[i];
342 }
343 }
344 let model = Model::new(&h, &g, &v, &dv, &scale_inv)?;
345 let theta = 0.995_f64.max(1. - gn);
346 let mut actual = -1.;
347 let mut xn = x.clone();
348 let mut fnn = f.clone();
349 let mut cn = cost;
350 while actual <= 0. && nfev < options.max_nfev {
351 let (ph, a, _, full_rank) =
352 trust(n, m, &model.uf, &model.s, &model.vectors, delta, alpha);
353 let mut trial = trace.as_ref().map(|_| NormalTrial {
354 x: x.clone(),
355 cost,
356 scale_inv: scale_inv.clone(),
357 v: v.clone(),
358 delta,
359 alpha_in: alpha,
360 alpha_out: a,
361 s: model.s.clone(),
362 full_rank,
363 ..NormalTrial::default()
364 });
365 alpha = a;
366 let p = ph.iter().zip(&model.d).map(|(p, d)| p * d).collect();
367 let (step, sh, pred) = select(
368 &x,
369 QuadraticModel::Normal(&model.h),
370 &model.diag,
371 &model.gh,
372 p,
373 ph,
374 &model.d,
375 delta,
376 lb,
377 ub,
378 theta,
379 trial.as_mut().map(|t| &mut t.select),
380 );
381 xn = x.iter().zip(&step).map(|(x, s)| x + s).collect();
382 feasible(&mut xn, lb, ub, false);
383 fnn = problem.residual(&xn);
384 nfev += 1;
385 let shn = norm(&sh);
386 if fnn.len() != m {
387 return Err(crate::invalid("residual length changed"));
388 }
389 if fnn.iter().any(|f| !f.is_finite()) {
390 if let (Some(t), Some(mut trial)) = (trace.as_deref_mut(), trial) {
391 trial.step = step;
392 trial.x_new = xn.clone();
393 trial.predicted_reduction = pred;
394 trial.actual_reduction = f64::NAN;
395 trial.ratio = f64::NAN;
396 trial.delta_new = 0.25 * shn;
397 t.trials.push(trial);
398 }
399 delta = 0.25 * shn;
400 continue;
401 }
402 cn = 0.5 * dot(&fnn, &fnn);
403 actual = cost - cn;
404 let ratio = if pred > 0. {
405 actual / pred
406 } else if pred == 0. && actual == 0. {
407 1.
408 } else {
409 0.
410 };
411 let dn = if ratio < 0.25 {
412 0.25 * shn
413 } else if ratio > 0.75 && shn > 0.95 * delta {
414 2. * delta
415 } else {
416 delta
417 };
418 if let (Some(t), Some(mut trial)) = (trace.as_deref_mut(), trial) {
419 trial.step = step.clone();
420 trial.x_new = xn.clone();
421 trial.predicted_reduction = pred;
422 trial.actual_reduction = actual;
423 trial.ratio = ratio;
424 trial.delta_new = dn;
425 trial.accepted = actual > 0.;
426 t.trials.push(trial);
427 }
428 status = termination(actual, cost, &step, &x, ratio, options.ftol, options.xtol);
429 if status != 0 {
430 break;
431 }
432 alpha *= delta / dn;
433 delta = dn;
434 }
435 if actual > 0. {
436 x = xn;
437 f = fnn;
438 cost = cn;
439 (h, g) = checked_normal(problem, &x, &f)?;
442 njev += 1;
443 if options.x_scale == XScale::Jac {
444 scale_inv = jac_scale(&h, Some(&scale_inv));
445 }
446 }
447 };
448 Ok(LeastSquaresResult {
449 x,
450 cost,
451 nfev,
452 status,
453 njev,
454 optimality,
455 })
456}