1use super::*;
4
5pub fn least_squares<R, J>(
7 residual: R,
8 jacobian: J,
9 mut x: Vec<f64>,
10 plan: &LeastSquaresPlan,
11) -> Result<LeastSquaresResult, Error>
12where
13 R: Fn(&[f64]) -> Vec<f64>,
14 J: Fn(&[f64]) -> Vec<Vec<f64>>,
15{
16 let n = x.len();
17 validate_scale(&plan.variable_scale, n)?;
18 if plan.initial_damping <= 0.0 || !plan.initial_damping.is_finite() {
19 return Err(Error::InvalidPlan(
20 "initial damping must be finite and positive",
21 ));
22 }
23 if let Some(bounds) = &plan.bounds {
24 if bounds.lower.len() != n {
25 return Err(Error::Dimension("bounds and point differ"));
26 }
27 bounds.project(&mut x);
28 if plan.policy != StepPolicy::TrustRegionReflective {
29 return Err(Error::InvalidPlan(
30 "bounded least squares requires trust-region reflective policy",
31 ));
32 }
33 } else if plan.policy != StepPolicy::LevenbergMarquardt {
34 return Err(Error::InvalidPlan(
35 "unconstrained least squares requires LM",
36 ));
37 }
38 let mut lambda = plan.initial_damping.max(1e-12);
39 let mut evals = 0;
40 for iter in 0..plan.limits.iterations {
41 if evals >= plan.limits.evaluations {
42 return Ok(ls_result(
43 x,
44 &residual,
45 &jacobian,
46 plan.bounds.as_ref(),
47 Termination::WorkLimit,
48 evals,
49 iter,
50 false,
51 ));
52 }
53 let r = residual(&x);
54 let j = jacobian(&x);
55 evals += 2;
56 if plan.residual_scale.len() != r.len()
57 || plan
58 .residual_scale
59 .iter()
60 .any(|v| !v.is_finite() || *v <= 0.0)
61 || j.len() != r.len()
62 || j.iter().any(|v| v.len() != n)
63 {
64 return Err(Error::Dimension(
65 "residual scaling or Jacobian dimensions differ",
66 ));
67 }
68 let memory = (j.len() * n + n * n) * 8;
69 if memory > plan.limits.memory_bytes {
70 return Ok(ls_result(
71 x,
72 &residual,
73 &jacobian,
74 plan.bounds.as_ref(),
75 Termination::WorkLimit,
76 evals,
77 iter,
78 false,
79 ));
80 }
81 if !finite(&r) || j.iter().any(|v| !finite(v)) {
82 return Ok(ls_result(
83 x,
84 &residual,
85 &jacobian,
86 plan.bounds.as_ref(),
87 Termination::NonFinite,
88 evals,
89 iter,
90 false,
91 ));
92 }
93 let mut step = if let Some(bounds) = &plan.bounds {
94 reflective_step(
95 &j,
96 &r,
97 &x,
98 bounds,
99 lambda,
100 (1.0 / lambda).sqrt(),
101 plan.tolerances.gradient,
102 )
103 } else {
104 let (a, b) = normal(&j, &r, lambda);
105 solve(a, b, plan.tolerances.gradient).0
106 };
107 if norm(&step) <= plan.tolerances.argument {
108 return Ok(ls_result(
109 x,
110 &residual,
111 &jacobian,
112 plan.bounds.as_ref(),
113 Termination::Converged,
114 evals,
115 iter,
116 false,
117 ));
118 }
119 if let Some(bounds) = &plan.bounds {
120 for i in 0..n {
124 let room = if step[i] > 0.0 {
125 bounds.upper[i] - x[i]
126 } else {
127 x[i] - bounds.lower[i]
128 };
129 if step[i].abs() > room {
130 step[i] = (0.995 * room).copysign(step[i])
131 }
132 }
133 }
134 let y = x.iter().zip(&step).map(|(a, b)| a + b).collect::<Vec<_>>();
135 let nr = residual(&y);
136 evals += 1;
137 if !finite(&nr) {
138 lambda *= 10.0;
139 continue;
140 }
141 if norm(&nr) < norm(&r) {
142 x = y;
143 lambda *= 0.3;
144 if (norm(&r) - norm(&nr)).abs() <= plan.tolerances.objective {
145 return Ok(ls_result(
146 x,
147 &residual,
148 &jacobian,
149 plan.bounds.as_ref(),
150 Termination::Converged,
151 evals,
152 iter + 1,
153 false,
154 ));
155 }
156 } else {
157 lambda *= 10.0
158 }
159 }
160 Ok(ls_result(
161 x,
162 &residual,
163 &jacobian,
164 plan.bounds.as_ref(),
165 Termination::WorkLimit,
166 evals,
167 plan.limits.iterations,
168 false,
169 ))
170}
171
172pub fn linear_least_squares(
179 a: &[Vec<f64>],
180 b: &[f64],
181 bounds: Bounds,
182 tol: f64,
183 limits: Limits,
184 statistical_assumptions: bool,
185) -> Result<LeastSquaresResult, Error> {
186 let n = a.first().map_or(0, Vec::len);
187 if a.len() != b.len() || bounds.lower.len() != n || a.iter().any(|r| r.len() != n) {
188 return Err(Error::Dimension("linear system dimensions differ"));
189 }
190 if !tol.is_finite() || tol <= 0.0 || !finite(b) || a.iter().any(|r| !finite(r)) {
191 return Err(Error::InvalidPlan(
192 "linear least-squares data and tolerance must be finite",
193 ));
194 }
195 let memory = a.len().saturating_mul(n).saturating_mul(24);
196 if memory > limits.memory_bytes {
197 return Ok(linear_result(
198 a,
199 b,
200 vec![0.0; n],
201 0,
202 Vec::new(),
203 0,
204 statistical_assumptions,
205 Termination::WorkLimit,
206 ));
207 }
208 let mut x = bounds
209 .lower
210 .iter()
211 .zip(&bounds.upper)
212 .map(|(l, u)| 0.0_f64.clamp(*l, *u))
213 .collect::<Vec<_>>();
214 let mut active = vec![None; n]; let mut iterations = 0;
216 for k in 0..limits.iterations {
217 iterations = k + 1;
218 let free = (0..n).filter(|&i| active[i].is_none()).collect::<Vec<_>>();
219 let adjusted = a
220 .iter()
221 .zip(b)
222 .map(|(row, rhs)| {
223 rhs - (0..n)
224 .filter(|&i| active[i].is_some())
225 .map(|i| row[i] * x[i])
226 .sum::<f64>()
227 })
228 .collect::<Vec<_>>();
229 let reduced = a
230 .iter()
231 .flat_map(|row| free.iter().map(|&i| row[i]))
232 .collect::<Vec<_>>();
233 let (candidate, rank) = if free.is_empty() {
234 (Vec::new(), 0)
235 } else {
236 let svd = svd_f64(
237 &reduced,
238 a.len(),
239 free.len(),
240 SvdPlan {
241 max_dimension: a.len().max(free.len()),
242 max_work: u64::try_from(limits.evaluations)
243 .unwrap_or(u64::MAX)
244 .saturating_mul(1_000),
245 max_iterations: limits.iterations.max(1),
246 tolerance: tol,
247 vectors: VectorForm::Thin,
248 reconstruction_tolerance: tol.sqrt().max(1e-10),
249 return_partial: false,
250 },
251 )
252 .map_err(|_| {
253 Error::InvalidPlan("SVD could not certify the reduced least-squares system")
254 })?;
255 let rank = numerical_rank(&svd, SingularCutoff(tol))
256 .map_err(|_| Error::InvalidPlan("invalid SVD cutoff"))?;
257 let z = svd_least_squares(&svd, &adjusted, SingularCutoff(tol))
258 .map_err(|_| Error::InvalidPlan("SVD least-squares solve failed"))?;
259 (z, rank)
260 };
261 let mut target = x.clone();
262 for (&i, &z) in free.iter().zip(&candidate) {
263 target[i] = z;
264 }
265 let mut alpha = 1.0_f64;
266 let mut hit = None;
267 for &i in &free {
268 let step = target[i] - x[i];
269 let (bound, upper) = if step > 0.0 {
270 (bounds.upper[i], true)
271 } else {
272 (bounds.lower[i], false)
273 };
274 if step != 0.0 {
275 let q = (bound - x[i]) / step;
276 if q >= 0.0 && q < alpha {
277 alpha = q;
278 hit = Some((i, upper));
279 }
280 }
281 }
282 for &i in &free {
283 x[i] += alpha * (target[i] - x[i]);
284 }
285 if let Some((i, upper)) = hit {
286 x[i] = if upper {
287 bounds.upper[i]
288 } else {
289 bounds.lower[i]
290 };
291 active[i] = Some(upper);
292 continue;
293 }
294
295 let residual = linear_residual(a, b, &x);
296 let gradient = (0..n)
297 .map(|j| {
298 a.iter()
299 .zip(&residual)
300 .map(|(row, r)| row[j] * r)
301 .sum::<f64>()
302 })
303 .collect::<Vec<_>>();
304 let release = (0..n)
305 .filter(|&i| match active[i] {
306 Some(false) => gradient[i] < -tol,
307 Some(true) => gradient[i] > tol,
308 None => false,
309 })
310 .max_by(|&i, &j| gradient[i].abs().total_cmp(&gradient[j].abs()));
311 if let Some(i) = release {
312 active[i] = None;
313 } else {
314 let indices = (0..n).filter(|&i| active[i].is_some()).collect::<Vec<_>>();
315 let termination = if indices.is_empty() {
316 Termination::Converged
317 } else {
318 Termination::BoundaryConverged
319 };
320 return Ok(linear_result(
321 a,
322 b,
323 x,
324 rank + indices.len(),
325 indices,
326 iterations,
327 statistical_assumptions,
328 termination,
329 ));
330 }
331 if iterations >= limits.evaluations {
332 let indices = (0..n).filter(|&i| active[i].is_some()).collect::<Vec<_>>();
333 return Ok(linear_result(
334 a,
335 b,
336 x,
337 rank,
338 indices,
339 iterations,
340 statistical_assumptions,
341 Termination::WorkLimit,
342 ));
343 }
344 }
345 let active = (0..n)
346 .filter(|&i| (x[i] - bounds.lower[i]).abs() <= tol || (x[i] - bounds.upper[i]).abs() <= tol)
347 .collect();
348 Ok(linear_result(
349 a,
350 b,
351 x,
352 n,
353 active,
354 iterations,
355 statistical_assumptions,
356 Termination::WorkLimit,
357 ))
358}
359pub(crate) fn linear_residual(a: &[Vec<f64>], b: &[f64], x: &[f64]) -> Vec<f64> {
360 a.iter()
361 .zip(b)
362 .map(|(row, y)| row.iter().zip(x).map(|(v, z)| v * z).sum::<f64>() - y)
363 .collect()
364}
365#[allow(clippy::too_many_arguments)]
366pub(crate) fn linear_result(
367 a: &[Vec<f64>],
368 b: &[f64],
369 x: Vec<f64>,
370 rank: usize,
371 active: Vec<usize>,
372 iterations: usize,
373 stats: bool,
374 termination: Termination,
375) -> LeastSquaresResult {
376 let r = linear_residual(a, b, &x);
377 let n = x.len();
378 let covariance = if rank < n {
379 Covariance::Unavailable(CovarianceUnavailable::RankDeficient)
380 } else if !stats {
381 Covariance::Unavailable(CovarianceUnavailable::StatisticalAssumptionsNotDeclared)
382 } else if a.len() <= n {
383 Covariance::Unavailable(CovarianceUnavailable::InsufficientDegreesOfFreedom)
384 } else {
385 let mut gram = vec![vec![0.0; n]; n];
386 for row in a {
387 for i in 0..n {
388 for j in 0..n {
389 gram[i][j] += row[i] * row[j];
390 }
391 }
392 }
393 match inverse(gram, 1e-12) {
394 Some(mut inv) => {
395 let variance = r.iter().map(|v| v * v).sum::<f64>() / (a.len() - n) as f64;
396 for row in &mut inv {
397 for v in row {
398 *v *= variance;
399 }
400 }
401 Covariance::Available(inv)
402 }
403 None => Covariance::Unavailable(CovarianceUnavailable::RankDeficient),
404 }
405 };
406 LeastSquaresResult {
407 point: x,
408 residual_norm: norm(&r),
409 residuals: r,
410 rank,
411 active,
412 covariance,
413 termination,
414 work: Work {
415 evaluations: iterations,
416 iterations,
417 memory_bytes: (a.len() * n + n * n) * 8,
418 },
419 }
420}
421
422pub(crate) fn inverse(mut a: Vec<Vec<f64>>, tol: f64) -> Option<Vec<Vec<f64>>> {
423 let n = a.len();
424 let mut inv = vec![vec![0.0; n]; n];
425 for (i, row) in inv.iter_mut().enumerate() {
426 row[i] = 1.0
427 }
428 for k in 0..n {
429 let p = (k..n).max_by(|&i, &j| a[i][k].abs().total_cmp(&a[j][k].abs()))?;
430 if a[p][k].abs() <= tol {
431 return None;
432 }
433 a.swap(k, p);
434 inv.swap(k, p);
435 let d = a[k][k];
436 for j in 0..n {
437 a[k][j] /= d;
438 inv[k][j] /= d
439 }
440 for i in 0..n {
441 if i != k {
442 let q = a[i][k];
443 for j in 0..n {
444 a[i][j] -= q * a[k][j];
445 inv[i][j] -= q * inv[k][j]
446 }
447 }
448 }
449 }
450 Some(inv)
451}
452#[allow(clippy::too_many_arguments)]
453pub(crate) fn ls_result<R: Fn(&[f64]) -> Vec<f64>, J: Fn(&[f64]) -> Vec<Vec<f64>>>(
454 x: Vec<f64>,
455 r: &R,
456 j: &J,
457 bounds: Option<&Bounds>,
458 t: Termination,
459 e: usize,
460 i: usize,
461 stats: bool,
462) -> LeastSquaresResult {
463 let rv = r(&x);
464 let jj = j(&x);
465 let (_, rank) = solve(normal(&jj, &rv, 0.0).0, vec![0.0; x.len()], 1e-10);
466 let n = x.len();
467 let active = bounds.map_or_else(Vec::new, |bounds| {
468 (0..n)
469 .filter(|&i| {
470 (x[i] - bounds.lower[i]).abs() <= 10.0 * f64::EPSILON.sqrt()
471 || (x[i] - bounds.upper[i]).abs() <= 10.0 * f64::EPSILON.sqrt()
472 })
473 .collect()
474 });
475 LeastSquaresResult {
476 point: x,
477 residual_norm: norm(&rv),
478 residuals: rv,
479 rank,
480 active,
481 covariance: if rank < n {
482 Covariance::Unavailable(CovarianceUnavailable::RankDeficient)
483 } else if stats {
484 Covariance::Available(vec![vec![0.0; n]; n])
485 } else {
486 Covariance::Unavailable(CovarianceUnavailable::StatisticalAssumptionsNotDeclared)
487 },
488 termination: t,
489 work: Work {
490 evaluations: e,
491 iterations: i,
492 memory_bytes: jj.len() * n * 8,
493 },
494 }
495}