1#![doc = include_str!("../README.md")]
2#![no_std] use core::borrow::{Borrow, BorrowMut};
5use core::ops::Sub;
6
7#[cfg(feature = "alloc")]
8use dyn_problem::DynOptimizationProblem;
9use generic_array::{ArrayLength, GenericArray};
10use models::{FitModel, FitModelErrors};
11
12use const_problem::ConstOptimizationProblem;
13use generic_array_storage::{Conv, GenericArrayStorage, GenericMatrix, GenericMatrixFromExt};
14use levenberg_marquardt::LeastSquaresProblem;
15use nalgebra::allocator::{Allocator, Reallocator};
16#[cfg(feature = "alloc")]
17use nalgebra::Dyn;
18use nalgebra::{DefaultAllocator, Dim, DimMax, DimMaximum, DimMin, DimName, MatrixView};
19pub use simba::scalar::{ComplexField, RealField};
20
21use nalgebra::{Matrix, OMatrix};
22use typenum::Unsigned;
23
24use num_traits::{Float, NumCast};
25
26pub use levenberg_marquardt::LevenbergMarquardt;
29pub use levenberg_marquardt::MinimizationReport;
32pub use levenberg_marquardt::TerminationReason;
35
36pub mod models;
38
39#[doc(hidden)]
40mod const_problem;
41
42#[cfg(feature = "alloc")]
43#[doc(hidden)]
44mod dyn_problem;
45
46#[doc = include_str!("../doc/as_matrix_view.md")]
47pub trait AsMatrixView {
48 type Scalar;
50 type Points: CreateProblem;
52
53 fn convert(
55 &self,
56 ) -> MatrixView<'_, Self::Scalar, <Self::Points as CreateProblem>::Nalg, nalgebra::U1>;
57}
58
59impl<T: AsMatrixView + ?Sized> AsMatrixView for &T {
60 type Scalar = T::Scalar;
61 type Points = T::Points;
62
63 fn convert(
64 &self,
65 ) -> MatrixView<'_, Self::Scalar, <Self::Points as CreateProblem>::Nalg, nalgebra::U1> {
66 <T as AsMatrixView>::convert(self)
67 }
68}
69
70impl<T: AsMatrixView + ?Sized> AsMatrixView for &mut T {
71 type Scalar = T::Scalar;
72 type Points = T::Points;
73
74 fn convert(
75 &self,
76 ) -> MatrixView<'_, Self::Scalar, <Self::Points as CreateProblem>::Nalg, nalgebra::U1> {
77 <T as AsMatrixView>::convert(self)
78 }
79}
80
81impl<Scalar: nalgebra::Scalar, const N: usize> AsMatrixView for [Scalar; N]
82where
83 nalgebra::Const<N>: CreateProblem<Nalg = nalgebra::Const<N>>,
84{
85 type Scalar = Scalar;
86 type Points = nalgebra::Const<N>;
87
88 fn convert(
89 &self,
90 ) -> MatrixView<'_, Self::Scalar, <Self::Points as CreateProblem>::Nalg, nalgebra::U1> {
91 MatrixView::<'_, Self::Scalar, <Self::Points as CreateProblem>::Nalg, nalgebra::U1>::from_slice(self)
92 }
93}
94
95#[cfg(feature = "alloc")]
96impl<Scalar: nalgebra::Scalar> AsMatrixView for [Scalar] {
97 type Scalar = Scalar;
98 type Points = Dyn;
99
100 fn convert(&self) -> MatrixView<'_, Self::Scalar, Self::Points, nalgebra::U1> {
101 MatrixView::<'_, Self::Scalar, Self::Points, nalgebra::U1>::from_slice(self, self.len())
102 }
103}
104
105impl<Scalar, Points: Dim, S> AsMatrixView for Matrix<Scalar, Points, nalgebra::U1, S>
106where
107 Points: CreateProblem<Nalg = Points>,
108 S: nalgebra::storage::RawStorage<Scalar, Points, RStride = nalgebra::U1, CStride = Points>,
109{
110 type Scalar = Scalar;
111 type Points = Points;
112
113 fn convert(&self) -> MatrixView<'_, Self::Scalar, Self::Points, nalgebra::U1> {
114 self.as_view()
115 }
116}
117
118impl<Scalar: nalgebra::Scalar, Points: ArrayLength + Conv<TNum = Points>> AsMatrixView
119 for GenericArray<Scalar, Points>
120where
121 <Points as Conv>::Nalg: CreateProblem,
122 <<Points as Conv>::Nalg as CreateProblem>::Nalg: DimName,
123{
124 type Scalar = Scalar;
125 type Points = <Points as Conv>::Nalg;
126
127 fn convert(
128 &self,
129 ) -> MatrixView<'_, Self::Scalar, <Self::Points as CreateProblem>::Nalg, nalgebra::U1> {
130 MatrixView::<'_, Self::Scalar, <Self::Points as CreateProblem>::Nalg, nalgebra::U1>::from_slice(self)
131 }
132}
133
134pub trait CreateProblem {
138 type Nalg: Dim;
140
141 fn create<'d, Model: FitModel + 'd>(
143 x: MatrixView<'d, Model::Scalar, Self::Nalg, nalgebra::U1>,
144 y: MatrixView<'d, Model::Scalar, Self::Nalg, nalgebra::U1>,
145 model: Model,
146 weights: impl Fn(Model::Scalar, Model::Scalar) -> Model::Scalar + 'd,
147 ) -> impl LeastSquaresProblem<Model::Scalar, Self::Nalg, <Model::ParamCount as Conv>::Nalg> + 'd
148 where
149 Model::Scalar: ComplexField + Copy,
150 DefaultAllocator: Allocator<<Model::ParamCount as Conv>::Nalg, nalgebra::U1>,
151 DefaultAllocator: Allocator<Self::Nalg, nalgebra::U1>,
152 DefaultAllocator: Allocator<Self::Nalg, <Model::ParamCount as Conv>::Nalg>;
153}
154
155impl<const POINTS: usize> CreateProblem for typenum::Const<POINTS>
156where
157 Self: Conv<Nalg = nalgebra::Const<POINTS>>,
158{
159 type Nalg = nalgebra::Const<POINTS>;
160
161 fn create<'d, Model: FitModel + 'd>(
162 x: MatrixView<'d, Model::Scalar, Self::Nalg, nalgebra::U1>,
163 y: MatrixView<'d, Model::Scalar, Self::Nalg, nalgebra::U1>,
164 model: Model,
165 weights: impl Fn(Model::Scalar, Model::Scalar) -> Model::Scalar + 'd,
166 ) -> impl LeastSquaresProblem<Model::Scalar, Self::Nalg, <Model::ParamCount as Conv>::Nalg> + 'd
167 where
168 Model::Scalar: ComplexField + Copy,
169 DefaultAllocator: Allocator<<Model::ParamCount as Conv>::Nalg, nalgebra::U1>,
170 DefaultAllocator: Allocator<Self::Nalg, nalgebra::U1>,
171 DefaultAllocator: Allocator<Self::Nalg, <Model::ParamCount as Conv>::Nalg>,
172 {
173 ConstOptimizationProblem::<'d, Self, Model, _> {
174 model,
175 x,
176 y,
177 weights,
178 }
179 }
180}
181
182impl<const POINTS: usize> CreateProblem for nalgebra::Const<POINTS>
183where
184 Self: Conv<Nalg = Self>,
185{
186 type Nalg = Self;
187
188 fn create<'d, Model: FitModel + 'd>(
189 x: MatrixView<'d, Model::Scalar, Self::Nalg, nalgebra::U1>,
190 y: MatrixView<'d, Model::Scalar, Self::Nalg, nalgebra::U1>,
191 model: Model,
192 weights: impl Fn(Model::Scalar, Model::Scalar) -> Model::Scalar + 'd,
193 ) -> impl LeastSquaresProblem<Model::Scalar, Self::Nalg, <Model::ParamCount as Conv>::Nalg> + 'd
194 where
195 Model::Scalar: ComplexField + Copy,
196 DefaultAllocator: Allocator<<Model::ParamCount as Conv>::Nalg, nalgebra::U1>,
197 DefaultAllocator: Allocator<Self::Nalg, nalgebra::U1>,
198 DefaultAllocator: Allocator<Self::Nalg, <Model::ParamCount as Conv>::Nalg>,
199 {
200 ConstOptimizationProblem::<'d, Self, Model, _> {
201 model,
202 x,
203 y,
204 weights,
205 }
206 }
207}
208
209#[cfg(feature = "alloc")]
210impl CreateProblem for Dyn {
211 type Nalg = Self;
212
213 fn create<'d, Model: FitModel + 'd>(
214 x: MatrixView<'d, Model::Scalar, Self::Nalg, nalgebra::U1>,
215 y: MatrixView<'d, Model::Scalar, Self::Nalg, nalgebra::U1>,
216 model: Model,
217 weights: impl Fn(Model::Scalar, Model::Scalar) -> Model::Scalar + 'd,
218 ) -> impl LeastSquaresProblem<Model::Scalar, Self::Nalg, <Model::ParamCount as Conv>::Nalg> + 'd
219 where
220 Model::Scalar: ComplexField + Copy,
221 DefaultAllocator: Allocator<<Model::ParamCount as Conv>::Nalg, nalgebra::U1>,
222 DefaultAllocator: Allocator<Self::Nalg, nalgebra::U1>,
223 DefaultAllocator: Allocator<Self::Nalg, <Model::ParamCount as Conv>::Nalg>,
224 {
225 DynOptimizationProblem::<'d, Model, _> {
226 model,
227 x,
228 y,
229 weights,
230 }
231 }
232}
233
234#[doc(hidden)]
236#[allow(missing_debug_implementations)]
237pub struct FitterUnit(());
238
239type DataPoints<Data> = <<Data as AsMatrixView>::Points as CreateProblem>::Nalg;
240
241pub trait FitBound<Model: FitModel, X, Y = X>
245where
246 Model::Scalar: RealField,
247{
248 #[doc(hidden)]
249 type Points: Dim;
250 #[doc(hidden)]
251 fn fit(
252 minimizer: impl Borrow<LevenbergMarquardt<Model::Scalar>>,
253 model: &mut Model,
254 x: impl Borrow<X>,
255 y: impl Borrow<Y>,
256 weights: impl Fn(Model::Scalar, Model::Scalar) -> Model::Scalar,
257 ) -> MinimizationReport<Model::Scalar>;
258}
259
260impl<Model, X, Y> FitBound<Model, X, Y> for FitterUnit
261where
262 Model: FitModel,
263 Model::Scalar: RealField + Float,
264 Model::ParamCount: Conv,
265 <Model::ParamCount as Conv>::TNum: Sub<typenum::U1>,
266
267 DefaultAllocator: Allocator<<Model::ParamCount as Conv>::Nalg>,
268 DefaultAllocator: Allocator<DataPoints<X>>,
269 DefaultAllocator: Allocator<DataPoints<X>, <Model::ParamCount as Conv>::Nalg>,
270
271 X: AsMatrixView<Scalar = Model::Scalar>,
272 X::Points: CreateProblem,
273
274 Y: AsMatrixView<Scalar = Model::Scalar, Points = X::Points>,
275
276 DataPoints<X>:
277 DimMax<<Model::ParamCount as Conv>::Nalg> + DimMin<<Model::ParamCount as Conv>::Nalg>,
278 <Model::ParamCount as Conv>::Nalg:
279 DimMax<<X::Points as CreateProblem>::Nalg> + DimMin<<X::Points as CreateProblem>::Nalg>,
280 DefaultAllocator: Reallocator<
281 Model::Scalar,
282 DataPoints<X>,
283 <Model::ParamCount as Conv>::Nalg,
284 DimMaximum<DataPoints<X>, <Model::ParamCount as Conv>::Nalg>,
285 <Model::ParamCount as Conv>::Nalg,
286 >,
287{
288 type Points = <<X as AsMatrixView>::Points as CreateProblem>::Nalg;
289
290 #[allow(
291 clippy::inline_always,
292 reason = "This function is used in a single place, and, in fact, wound not exist unless I wanted to extract the type bounds to a separate trait."
293 )]
294 #[inline(always)]
295 fn fit(
296 minimizer: impl Borrow<LevenbergMarquardt<Model::Scalar>>,
297 model: &mut Model,
298 x: impl Borrow<X>,
299 y: impl Borrow<Y>,
300 weights: impl Fn(Model::Scalar, Model::Scalar) -> Model::Scalar,
301 ) -> MinimizationReport<Model::Scalar> {
302 let x = x.borrow().convert();
303 let y = y.borrow().convert();
304
305 let problem = X::Points::create::<'_, &mut Model>(x, y, model.borrow_mut(), weights);
306 let (_, report) = LevenbergMarquardt::minimize::<
307 <Model::ParamCount as Conv>::Nalg,
308 DataPoints<X>,
309 _,
310 >(minimizer.borrow(), problem);
311 report
312 }
313}
314
315pub trait FitErrBound<Model: FitModelErrors, X, Y = X>: FitBound<Model, X, Y>
319where
320 Model::Scalar: RealField,
321{
322 #[doc(hidden)]
323 fn produce_stat(
324 model: impl Borrow<Model>,
325 report: MinimizationReport<Model::Scalar>,
326 x: X,
327 y: Y,
328 ) -> FitStat<Model>;
329}
330
331impl<Model, X, Y> FitErrBound<Model, X, Y> for FitterUnit
332where
333 Self: FitBound<Model, X, Y>,
334
335 Model: FitModelErrors,
336 Model::Scalar: RealField + Float,
337 Model::ParamCount: Conv,
338 <Model::ParamCount as Conv>::TNum: Sub<typenum::U1>,
339
340 X: AsMatrixView<Scalar = Model::Scalar>,
341 X::Points: CreateProblem,
342
343 Y: AsMatrixView<Scalar = Model::Scalar, Points = X::Points>,
344
345 DefaultAllocator: Allocator<<Model::ParamCount as Conv>::Nalg>
346 + Allocator<DataPoints<X>>
347 + Allocator<<Model::ParamCount as Conv>::Nalg, DataPoints<X>>
348 + Allocator<DataPoints<X>, <Model::ParamCount as Conv>::Nalg>
349 + Allocator<<Model::ParamCount as Conv>::Nalg, <Model::ParamCount as Conv>::Nalg>,
350{
351 #[allow(
352 clippy::inline_always,
353 reason = "This function is used in a single place, and, in fact, wound not exist unless I wanted to extract the type bounds to a separate trait."
354 )]
355 #[inline(always)]
356 fn produce_stat(
357 model: impl Borrow<Model>,
358 report: MinimizationReport<Model::Scalar>,
359 x: X,
360 y: Y,
361 ) -> FitStat<Model>
362 where
363 Model: FitModelErrors,
364 {
365 let model = model.borrow();
366 let x = x.convert();
367 let y = y.convert();
368
369 let points =
370 <Model::Scalar as NumCast>::from::<usize>(x.len()).expect("Too many data points");
371 let u_params = <<Model::ParamCount as Conv>::TNum as Unsigned>::USIZE;
372 let parameters =
373 <Model::Scalar as NumCast>::from::<usize>(u_params).expect("Too many parameters");
374 let s_y_2 = if points > parameters {
377 x.zip_map(&y, |xi, yi| {
378 let f_x = model.evaluate(&xi);
379 let dev = yi - f_x;
380 dev * dev
381 })
382 .sum()
383 / (points - parameters)
384 } else {
385 Model::Scalar::nan()
387 };
388 let s_y = Float::sqrt(s_y_2);
389 let jacobian = OMatrix::<Model::Scalar, <Model::ParamCount as Conv>::Nalg, DataPoints<X>>::from_iterator_generic(
391 Model::ParamCount::new_nalg(),
392 DataPoints::<X>::from_usize(x.len()),
393 x.iter().flat_map(|x| model.jacobian(x).into()),
394 );
395 let jj_t = jacobian.clone() * jacobian.transpose();
396 let covariance_matrix = jj_t.try_inverse().map(|jj_x| jj_x * s_y).map_or_else(
397 || {
398 let col = core::iter::repeat_n(Model::Scalar::nan(), u_params).collect();
400 let arr = core::iter::repeat_n(col, u_params).collect();
401 GenericMatrix::from_data(GenericArrayStorage(arr))
402 },
403 Matrix::into_generic_matrix,
404 );
405
406 let param_errors = (0usize..u_params)
407 .map(|i| Float::sqrt(covariance_matrix[(i, i)]))
408 .collect();
409 let errors = Model::with_errors(param_errors);
410
411 FitStat {
412 report,
413 reduced_chi2: s_y_2,
414 errors,
415 covariance_matrix,
416 }
417 }
418}
419
420#[doc(hidden)]
422pub fn default_weights<Scalar: num_traits::One>(_x: Scalar, _y: Scalar) -> Scalar {
423 Scalar::one()
424}
425
426#[macro_export]
427#[cfg(doc)]
428#[doc = include_str!("../doc/fit_macro.md")]
429macro_rules! fit {
430 (
431 $model:expr,
432 $x:expr,
433 $y:expr
434 $(, weights = $wights:expr)?
435 $(, minimizer = $minimizer:expr)?
436 ) => { ... };
437}
438
439#[macro_export]
440#[cfg(not(doc))]
441#[doc = include_str!("../doc/fit_macro.md")]
442macro_rules! fit {
443 ($model:expr, $x:expr, $y:expr $(, $par_name:ident = $par_value:expr) *) => {{
444 use ::nacfahi::default_weights;
445 let mut minimizer = &::nacfahi::LevenbergMarquardt::new();
446 let model = $model;
447 let x = $x;
448 let y = $y;
449 ::nacfahi::fit!(@ $($par_name = $par_value),*; model = model, x = x, y = y, minimizer = minimizer, weights = default_weights)
450 }};
451
452 (@ minimizer = $new_minimizer:expr $(, $par_name:ident = $par_value:expr) *; model = $model:ident, x = $x:ident, y = $y:ident, minimizer = $minimizer:ident, weights = $weights:ident) => {{
453 use ::core::borrow::Borrow;
454 let tmp = $new_minimizer;
455 $minimizer = tmp.borrow();
456 ::nacfahi::fit!(@ $($par_name = $par_value),*; model = $model, x = $x, y = $y, minimizer = $minimizer, weights = $weights)
457 }};
458
459 (@ weights = $new_weights:expr $(, $par_name:ident = $par_value:expr) *; model = $model:ident, x = $x:ident, y = $y:ident, minimizer = $minimizer:ident, weights = $weights:ident) => {{
460 let weights = $new_weights;
461 ::nacfahi::fit!(@ $($par_name = $par_value),*; model = $model, x = $x, y = $y, minimizer = $minimizer, weights = weights)
462 }};
463
464 (@; model = $model:ident, x = $x:ident, y = $y:ident, minimizer = $minimizer:ident, weights = $weights:ident) => {
465 ::nacfahi::fit($model, $x, $y, $minimizer, $weights)
466 };
467}
468
469#[must_use = "Minimization report is really important to check if approximation happened at all"]
473pub fn fit<Model, X, Y>(
474 model: &mut Model,
475 x: X,
476 y: Y,
477 minimizer: impl Borrow<LevenbergMarquardt<Model::Scalar>>,
478 weights: impl Fn(Model::Scalar, Model::Scalar) -> Model::Scalar,
479) -> MinimizationReport<Model::Scalar>
480where
481 Model: FitModel,
482 Model::Scalar: RealField,
483 FitterUnit: FitBound<Model, X, Y>,
484{
485 FitterUnit::fit(minimizer, model, x, y, weights)
486}
487
488#[derive(Debug)]
490pub struct FitStat<Model: FitModelErrors>
491where
492 Model::Scalar: RealField,
493{
494 pub report: MinimizationReport<Model::Scalar>,
496 pub reduced_chi2: Model::Scalar,
498 pub errors: Model::OwnedModel,
502 pub covariance_matrix: GenericMatrix<Model::Scalar, Model::ParamCount, Model::ParamCount>,
504}
505
506#[macro_export]
507#[cfg(doc)]
508#[doc = include_str!("../doc/fit_stat_macro.md")]
509macro_rules! fit_stat {
510 (
511 $model:expr,
512 $x:expr,
513 $y:expr
514 $(, weights = $wights:expr)?
515 $(, minimizer = $minimizer:expr)?
516 ) => { ... };
517}
518
519#[macro_export]
520#[cfg(not(doc))]
521#[doc = include_str!("../doc/fit_stat_macro.md")]
522macro_rules! fit_stat {
523 ($model:expr, $x:expr, $y:expr $(, $par_name:ident = $par_value:expr) *) => {{
524 use ::nacfahi::default_weights;
525 let mut minimizer = &::nacfahi::LevenbergMarquardt::new();
526 let model = $model;
527 let x = $x;
528 let y = $y;
529 ::nacfahi::fit_stat!(@ $($par_name = $par_value),*; model = model, x = x, y = y, minimizer = minimizer, weights = default_weights)
530 }};
531
532 (@ minimizer = $new_minimizer:expr $(, $par_name:ident = $par_value:expr) *; model = $model:ident, x = $x:ident, y = $y:ident, minimizer = $minimizer:ident, weights = $weights:ident) => {{
533 use ::core::borrow::Borrow;
534 let tmp = $new_minimizer;
535 $minimizer = tmp.borrow();
536 ::nacfahi::fit_stat!(@ $($par_name = $par_value),*; model = $model, x = $x, y = $y, minimizer = $minimizer, weights = $weights)
537 }};
538
539 (@ weights = $new_weights:expr $(, $par_name:ident = $par_value:expr) *; model = $model:ident, x = $x:ident, y = $y:ident, minimizer = $minimizer:ident, weights = $weights:ident) => {{
540 let weights = $new_weights;
541 ::nacfahi::fit_stat!(@ $($par_name = $par_value),*; model = $model, x = $x, y = $y, minimizer = $minimizer, weights = weights)
542 }};
543
544 (@; model = $model:ident, x = $x:ident, y = $y:ident, minimizer = $minimizer:ident, weights = $weights:ident) => {
545 ::nacfahi::fit_stat($model, $x, $y, $minimizer, $weights)
546 };
547}
548
549#[must_use = "Covariance matrix are the only point to call this function specifically"]
562pub fn fit_stat<Model, X, Y>(
563 model: &mut Model,
564 x: X,
565 y: Y,
566 minimizer: impl Borrow<LevenbergMarquardt<Model::Scalar>>,
567 weights: impl Fn(Model::Scalar, Model::Scalar) -> Model::Scalar,
568) -> FitStat<Model>
569where
570 Model: FitModelErrors,
571 Model::Scalar: RealField,
572 FitterUnit: FitErrBound<Model, X, Y>,
573{
574 let report = FitterUnit::fit(minimizer, model.borrow_mut(), &x, &y, weights);
575 FitterUnit::produce_stat(model, report, x, y)
576}