nacfahi/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std] // <-- see that attr? No shit!
3
4use 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
26/// Re-export. See [`LevenbergMarquardt`](https://docs.rs/levenberg-marquardt/latest/levenberg_marquardt/struct.LevenbergMarquardt.html)
27///
28pub use levenberg_marquardt::LevenbergMarquardt;
29/// Re-export. See [`MinimizationReport`](https://docs.rs/levenberg-marquardt/latest/levenberg_marquardt/struct.MinimizationReport.html)
30///
31pub use levenberg_marquardt::MinimizationReport;
32/// Re-export. See [`TerminationReason`](https://docs.rs/levenberg-marquardt/latest/levenberg_marquardt/enum.TerminationReason.html)
33///
34pub use levenberg_marquardt::TerminationReason;
35
36/// Fitting models
37pub 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 of the elements
49    type Scalar;
50    /// Type representing data points count.
51    type Points: CreateProblem;
52
53    /// Creates data view from type.
54    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
134/// Indicates *how exactly* matrix data views should be converted into [`LeastSquaresProblem`](https://docs.rs/levenberg-marquardt/latest/levenberg_marquardt/trait.LeastSquaresProblem.html) suitable for [`levenberg_marquardt`](https://docs.rs/levenberg-marquardt/latest/levenberg_marquardt/index.html) operation.
135///
136/// In case it seems pointless, there are actually currently two fundamentally different problem types corresponding to const (stack-based) and dyn (alloc-based) problems. They should be hidden from docs, but this trait's implementation points should get you started (in case you're curious, or something).
137pub trait CreateProblem {
138    /// [`nalgebra`]-facing type, for view size definition
139    type Nalg: Dim;
140
141    /// Creates a problem from data views and arbitrary model.
142    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/// A helper unit type that is never constructed, and only used in type bounds.
235#[doc(hidden)]
236#[allow(missing_debug_implementations)]
237pub struct FitterUnit(());
238
239type DataPoints<Data> = <<Data as AsMatrixView>::Points as CreateProblem>::Nalg;
240
241/// A helper trait to simplify type bounds for a user. You probably should no see this.
242///
243/// In case you do get a "type does not implement" type or error with this trait... I'm sorry.
244pub 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
315/// A helper trait to simplify type bounds for a user. You probably should no see this.
316///
317/// In case you do get a "type does not implement" type or error with this trait... I'm sorry.
318pub 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        // source: https://scholarsarchive.byu.edu/cgi/viewcontent.cgi?article=3213&context=facpub
375        // ch. 2 Estimating Uncertainties
376        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            // WARN: add trace event here, or something
386            Model::Scalar::nan()
387        };
388        let s_y = Float::sqrt(s_y_2);
389        // thing below is (J * J^T)^-1
390        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                // WARN: add trace here too, I guess
399                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/// Default weights function.
421#[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/// Main interface point. For more convenient use (mostly - to omit some of the fields), you might want to look into [`macro@fit!`] macro.
470///
471/// **TIP**: The [`FitBound`] is an unfortunate outcome to strict type system. In case you deal with generic code, just put the `fit!` statement down, and add the bound you seemingly violate - you **should** be good after that.
472#[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/// Result of [`function@fit_stat`].
489#[derive(Debug)]
490pub struct FitStat<Model: FitModelErrors>
491where
492    Model::Scalar: RealField,
493{
494    /// Report resulted from the fit
495    pub report: MinimizationReport<Model::Scalar>,
496    /// $\chi^{2}/\test{dof}$ criteria. Should be about 1 for correct fit.
497    pub reduced_chi2: Model::Scalar,
498    /// Type defined by model, containing parameter errors.
499    ///
500    /// This will usually be the model type itself, but there may be exceptions.
501    pub errors: Model::OwnedModel,
502    /// A parameter covariance matrix. If you don't know what this is, you can safely ignore it.
503    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/// Same as [`function@fit`], but outputs a bunch of other stuff alongside [`MinimizationReport`].
550///
551/// ### Outputs NaN
552///
553/// - If there are more less or the same number of data points than parameters. In this case, $\chi^{2}/\text{dof}$ is undefined, and consequently - rest of the analysis.
554/// - If
555///
556/// ### Panics
557///
558/// - If data points count can't be converted to scalar type
559///
560/// **TIP**: The `FitDimensionsBound` is an unfortunate outcome to strict type system. In case you deal with generic code, just put the `fit!` statement down, and add the bound you seemingly violate - you **should** be good after that.
561#[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}