Expand description
This crate does not require nor std nor alloc ❤️
This is my take on a “convenient interface” for a brilliant levenberg_marquardt
crate, aiming to be versatile, flexible and easy-to-understand.
Before you read further, consider giving levenberg_marquardt
itself a look - it’s interface is quite abstract, and you might come up with a more efficient use pattern for your problem.
As another shout out, see varpro
crate, which does basically the same thing (provides a high-level interface to levenberg_marquardt
), except it has some more special sauce your application might benefit from.
§Motivation
I’ve identified following drawbacks in APIs of mentioned crates:
levenberg_marquardt
usesnalgebra
and requires you to explicitly use it too. This can be quite confusing, especially if you are yet to readnalgebra
’s doc.- Both
levenberg_marquardt
andvarpro
unify parameters and data into a single object. This makes sense for internal solving process, but there’s no reason to leave it like that in a public API. varpro
requiresstd
(because reasons, I guess)levenberg_marquardt
allows problem to return any sort ofnalgebra
matrix, abstracted over storage.varpro
, on the other hand, is hard-coded to useDyn
-sized storages, meaning that for every single parameters get/set operation, new vector (on the heap!) is allocated. This might be less than ideal, especially if both parameter and point count happen to be statically known.varpro
defines it’sSeparableModel
parameters as analgebra
vector. This leaves your code prone to getting wrong parameter or non-existing parameter from the vector.
I’ve come up with the following design:
- All models have statically-known parameter count. This allows get/set operations to use stack-allocated storage.
- Model (parameters container) is separate from data. Combination into a single struct is not required to use to the public API.
- (2) allows definition of an alternative model trait, with no
nalgebra
mentioned. Instead, it tangentially mentionsgeneric_array
(specifically, there’s anInto<GenericArray>
return bound), which I find easier to use. - (2) allows abstraction over data-providing type. Data provider is converted into
nalgebra
matrix internally. - This crate only heap-allocates in case there is an unknown data point count.
- Model type is never erased, models expose relevant parameters as fields/methods. Makes impossible to ask for wrong/incorrect parameter, since all parameters are obtained via field access and function calls (instead of opaque
nalgebra
matrix element). - Crate provides a simple way to compose multiple models into a single, more complex one.
I find described API more intuitive and less error prone. Also it statically prevents some of the levenberg_marquardt
s termination reasons (User
, NoParameters
, NoResiduals
, WrongDimensions
).
With general idea outlined, here’s an example:
§Basic example
// some data, presumably 2x + 1
let x = [1.0, 2.0, 3.0, 4.0, 5.0];
let y = [3.0, 5.0, 7.0, 9.0, 11.0];
// fitting model: a*x + b
let mut line = Linear { a: 0.0, b: 0.0 };
// do the fit!
let report = fit!(&mut line, x, y);
// check that approximation is successful
assert!(
report.termination.was_successful(),
"Approximation should be successful"
);
// check that model parameters have expected values
assert_ulps_eq!(line.a, 2.0);
assert_ulps_eq!(line.b, 1.0);
Looks simple enough? Consider reading the rest, then!
§What’s actually going on
There are a couple things to unpack here:
§Data format
Input data can be any AsMatrixView
trait implementor, and core Rust array happens to be one. See trait’s documentation for details.
§fit
macro
That’s a pure convenience macro expanding into a fit
function call. For details, see fit!
.
§Fit models
Model generally refers to FitModel
implementation, internally wired to levenberg_marquardt
’s LeastSquaresProblem
trait.
Most of the public items in this crate are models representing various common fitting functions or meta operations.
Additionally, FitModel
is implemented for &mut
FitModel
- this is actually utilized in the above example to keep ownership of the model after the fit.
Also, core Rust arrays implement FitModel
, as a sum of it’s element models. So [Exponent; 2]
would fit with a sum of two independent Exponent
models, and [Gaussian; 5]
would fit with 5 Gaussian
independent models.
To reiterate: these models contain multiple independent instances of the same model type that are added up.
§Basic Models
Basic models are representations of elementary functions. You can fit them directly (as does the example above), or compose more complex models with them (see below).
Basic models are located in models::basic
module, see it’s items for details.
§Utility models
Utility models are models containing other models, implementing some sort of additional functionality, like range filtering or mapping.
See models::utility
items for details. Here are some examples:
Ranged
has a second fieldrange
definingx
variable range the model will be nonzero in. Sharp turns can be emulated with this model, for example here is an exponential “ramp”, dropping after0.0
:
// (see `utility_models` integration test for details)
type ExpRamp<Scalar> = Ranged<Exponent<Scalar>, RangeTo<Scalar>>;
// for example, this model equals 0 at x > 0:
let _ramp = Ranged {
inner: Exponent { a: 0.0, b: 0.0 },
range: ..0.0,
};
ModelMap
(UNTESTED!) is supposed to additionally map the model, allowing fits in mapped spaces. For example, while fitting to a single exponent, you might want to useLnMap
to do a linear fit:
// some exponential data
let expected_a = 3.0;
let expected_b = 0.5;
let x = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0];
let y = x.map(|x| expected_a * (x * expected_b).exp());
let linear_y = y.map(f64::ln);
// exponential model
let mut expo_model = Exponent { a: 1.0, b: 0.0 };
// expolinear (exponential mapped to linear)
let mut expolinear = model_map(&mut expo_model, LnMap);
// fit!
let report = fit!(&mut expolinear, x, linear_y);
Note: this functionality is largely unfinished, and probably should not be used yet
Composition
(UNTESTED!) is supposed to allow model composition. This is similar toModelMap
, except “the map” here has it’s own parameters and fitting process fits them as well. Here’s an example of gaussian-over-exponential model (whatever that would mean):
/* no example hewe, sowwy :( */
Note: this functionality is largely unfinished, and probably should not be used yet
§Custom models
What if you need a model representing a sum of linear, exponential and three gaussian peaks? Even [Box<dyn FitModel>; 5]
won’t work, since FitModel
is not object-safe…
Well, FitModel
is fully public, and you are free to implement it yourself! In this case, it’s a bunch of annoying boilerplate, you can find here (you can check it, in case you plan on implementing the trait yourself).
Good news is - there’s a derive
macro for that!
#[derive(FitModelSum)]
#[scalar_type(f64)]
struct ConstExponent {
linear: Constant<f64>,
exponent: Exponent<f64>,
peaks: [Gaussian<f64>; 3],
}
And it does exactly all of the above, except you can do some stuff that is hard to implement manually.
See FitModelSum
for usage details and more examples.
§Why the name?
Actual intended name is nacfa'i
, which is a lojban predicate for “x1 is solved to find x2”.
(I am not proficient in lojban at all, pwease don’t huwt mw :3
Modules§
- models
- Fitting models
Macros§
- fit
- A convenience macro for
fit
function. You are free to call the function directly, if this macro is confusing to you. - fit_
stat - Same as
fit!
, expect it computesFitStat
instead of simpleMinimizationReport
. - test_
model_ derivative - Generates a test to numerically check, if your model has a correct jacobian implementation.
Structs§
- FitStat
- Result of
fit_stat
. - Fitter
Unit - A helper unit type that is never constructed, and only used in type bounds.
- Generic
Array - Re-exports from
generic_array
Struct representing a generic array -GenericArray<T, N>
works like[T; N]
- Levenberg
Marquardt - Re-export. See
LevenbergMarquardt
Levenberg-Marquardt optimization algorithm. - Minimization
Report - Re-export. See
MinimizationReport
Information about the minimization.
Enums§
- Termination
Reason - Re-export. See
TerminationReason
Reasons for terminating the minimization.
Traits§
- AsMatrix
View - Defines valid types to provide data with.
- Complex
Field - Trait shared by all complex fields and its subfields (like real numbers).
- Concat
- Re-exports from
generic_array
DefinesGenericSequence
s which can be joined together, forming a larger array. - Conv
- Re-export from
generic_array_storage
Convenience trait, used to define type conversions - Create
Problem - Indicates how exactly matrix data views should be converted into
LeastSquaresProblem
suitable forlevenberg_marquardt
operation. - FitBound
- A helper trait to simplify type bounds for a user. You probably should no see this.
- FitErr
Bound - A helper trait to simplify type bounds for a user. You probably should no see this.
- Real
Field - Trait shared by all reals.
- Split
- Re-exports from
generic_array
Defines aGenericSequence
that can be split into two parts at a given pivot index.
Functions§
- fit
- Main interface point. For more convenient use (mostly - to omit some of the fields), you might want to look into
fit!
macro. - fit_
stat - Same as
fit
, but outputs a bunch of other stuff alongsideMinimizationReport
.
Type Aliases§
- Data
Points 🔒 - U
- Re-export from
typenum