• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-ant / varpro / 16667413409

01 Aug 2025 05:48AM UTC coverage: 76.836% (-1.6%) from 78.389%
16667413409

Pull #50

github

web-flow
Merge 8c21f317e into f242b6e14
Pull Request #50: Feature/documentation improvements 2

5 of 5 new or added lines in 1 file covered. (100.0%)

34 existing lines in 7 files now uncovered.

743 of 967 relevant lines covered (76.84%)

2688.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.35
/src/problem/builder.rs
1
use crate::prelude::*;
2
use crate::problem::SeparableProblem;
3
use crate::util::Weights;
4
use levenberg_marquardt::LeastSquaresProblem;
5
use nalgebra::{ComplexField, DMatrix, Dyn, OMatrix, OVector, Scalar};
6
use num_traits::{Float, Zero};
7
use std::ops::Mul;
8
use thiserror::Error as ThisError;
9

10
use super::{MultiRhs, RhsType, SingleRhs};
11

12
/// Errors pertaining to use errors of the `SeparableProblemBuilder`
13
#[derive(Debug, Clone, ThisError, PartialEq, Eq)]
14
#[allow(missing_docs)]
15
pub enum SeparableProblemBuilderError {
16
    /// the data for y variable was not given to the builder
17
    #[error("Right hand side(s) not provided")]
18
    YDataMissing,
19

20
    /// x and y vector have different lengths
21
    #[error(
22
        "Vectors x and y must have same lengths. Given x length = {} and y length = {}",
23
        x_length,
24
        y_length
25
    )]
26
    InvalidLengthOfData { x_length: usize, y_length: usize },
27

28
    /// the provided x and y vectors must not have zero elements
29
    #[error("x or y must have nonzero number of elements.")]
30
    ZeroLengthVector,
31

32
    /// the model has a different number of parameters than the provided initial guesses
33
    #[error(
34
        "Initial guess vector must have same length as parameters. Model has {} parameters and {} initial guesses were provided.",
35
        model_count,
36
        provided_count
37
    )]
38
    InvalidParameterCount {
39
        model_count: usize,
40
        provided_count: usize,
41
    },
42

43
    /// y vector and weights have different lengths
44
    #[error("The weights must have the same length as the data y.")]
45
    InvalidLengthOfWeights,
46
}
47

48
/// A builder structure to create a [SeparableProblem](super::SeparableProblem), which can be used for
49
/// fitting a separable model to data.
50
/// # Example
51
/// The following code shows how to create an unweighted least squares problem to fit the separable model
52
/// `$\vec{f}(\vec{x},\vec{\alpha},\vec{c})$` given by `model` to data `$\vec{y}$` (given as `y`) using the
53
/// independent variable `$\vec{x}$` (given as `x`). Furthermore we need an initial guess `params`
54
/// for the nonlinear model parameters `$\vec{\alpha}$`
55
/// ```rust
56
/// # use nalgebra::{DVector, Scalar};
57
/// # use varpro::model::SeparableModel;
58
/// # use varpro::problem::*;
59
/// # fn model(model : SeparableModel<f64>,y: DVector<f64>) {
60
///   let problem = SeparableProblemBuilder::new(model)
61
///                 .observations(y)
62
///                 .build()
63
///                 .unwrap();
64
/// # }
65
/// ```
66
///
67
/// # Building a Model
68
///
69
/// A new builder is constructed with the [new](SeparableProblemBuilder::new) constructor. It must be filled with
70
/// content using the methods described in the following. After all mandatory fields have been filled,
71
/// the [build](SeparableProblemBuilder::build) method can be called. This returns a [Result](std::result::Result)
72
/// type that contains the finished model iff all mandatory fields have been set with valid values. Otherwise
73
/// it contains an error variant.
74
///
75
/// ## Multiple Right Hand Sides
76
///
77
/// We can also construct a problem with multiple right hand sides, using the
78
/// [mrhs](SeparableProblemBuilder::mrhs) constructor, see [SeparableProblem] for
79
/// additional details.
80
#[derive(Clone)]
81
#[allow(non_snake_case)]
82
pub struct SeparableProblemBuilder<Model, Rhs: RhsType>
83
where
84
    Model::ScalarType: Scalar + ComplexField + Copy,
85
    <Model::ScalarType as ComplexField>::RealField: Float,
86
    Model: SeparableNonlinearModel,
87
{
88
    /// Required: the data `$\vec{y}(\vec{x})$` that we want to fit
89
    Y: Option<DMatrix<Model::ScalarType>>,
90
    /// Required: the model to be fitted to the data
91
    separable_model: Model,
92
    /// Optional: set epsilon below which two singular values
93
    /// are considered zero.
94
    /// if this is not given, when building the builder,
95
    /// the this is set to machine epsilon.
96
    epsilon: Option<<Model::ScalarType as ComplexField>::RealField>,
97
    /// Optional: weights to be applied to the data for weightes least squares
98
    /// if no weights are given, the problem is unweighted, i.e. the same as if
99
    /// all weights were 1.
100
    /// Must have the same length as x and y.
101
    weights: Weights<Model::ScalarType, Dyn>,
102
    phantom: std::marker::PhantomData<Rhs>,
103
}
104

105
impl<Model> SeparableProblemBuilder<Model, SingleRhs>
106
where
107
    Model::ScalarType: Scalar + ComplexField + Zero + Copy,
108
    <Model::ScalarType as ComplexField>::RealField: Float,
109
    <<Model as SeparableNonlinearModel>::ScalarType as ComplexField>::RealField:
110
        Mul<Model::ScalarType, Output = Model::ScalarType> + Float,
111
    Model: SeparableNonlinearModel,
112
{
113
    /// Create a new builder based on the given model for a problem
114
    /// with a **single right hand side**. This is the standard use case,
115
    /// where the data is a vector that is fitted by the model.
116
    pub fn new(model: Model) -> Self {
24✔
117
        Self {
118
            Y: None,
119
            separable_model: model,
120
            epsilon: None,
121
            weights: Weights::default(),
24✔
122
            phantom: Default::default(),
24✔
123
        }
124
    }
125
}
126

127
impl<Model> SeparableProblemBuilder<Model, SingleRhs>
128
where
129
    Model::ScalarType: Scalar + ComplexField + Zero + Copy,
130
    <Model::ScalarType as ComplexField>::RealField: Float,
131
    <<Model as SeparableNonlinearModel>::ScalarType as ComplexField>::RealField:
132
        Mul<Model::ScalarType, Output = Model::ScalarType> + Float,
133
    Model: SeparableNonlinearModel,
134
{
135
    /// **Mandatory**: Set the data which we want to fit: since this
136
    /// is called on a model builder for problems with **single right hand sides**,
137
    /// this is a column vector `$\vec{y}=\vec{y}(\vec{x})$` containing the
138
    /// values we want to fit with the model.
139
    ///
140
    /// The length of `$\vec{y}` and the output dimension of the model must be
141
    /// the same.
142
    pub fn observations(self, observed: OVector<Model::ScalarType, Dyn>) -> Self {
22✔
143
        let nrows = observed.nrows();
66✔
144
        Self {
145
            Y: Some(observed.reshape_generic(Dyn(nrows), Dyn(1))),
66✔
146
            ..self
147
        }
148
    }
149
}
150

151
impl<Model> SeparableProblemBuilder<Model, MultiRhs>
152
where
153
    Model::ScalarType: Scalar + ComplexField + Zero + Copy,
154
    <Model::ScalarType as ComplexField>::RealField: Float,
155
    <<Model as SeparableNonlinearModel>::ScalarType as ComplexField>::RealField:
156
        Mul<Model::ScalarType, Output = Model::ScalarType> + Float,
157
    Model: SeparableNonlinearModel,
158
{
159
    /// Create a new builder based on the given model
160
    /// for a problem with **multiple right hand sides** and perform a global
161
    /// fit. This uses single threaded calculations.
162
    ///
163
    /// That means the observations are expected to be a matrix, where the
164
    /// columns correspond to the individual observations.
165
    ///
166
    /// For a set of observations `$\vec{y}_1,\dots,\vec{y}_S$` (column vectors) we
167
    /// now have to pass a _matrix_ `$Y$` of observations, rather than a single
168
    /// vector to the builder. As explained above, the resulting matrix would look
169
    /// like this.
170
    ///
171
    /// ```math
172
    /// \boldsymbol{Y}=\left(\begin{matrix}
173
    ///  \vert &  & \vert \\
174
    ///  \vec{y}_1 &  \dots & \vec{y}_S \\
175
    ///  \vert &  & \vert \\
176
    /// \end{matrix}\right)
177
    /// ```
178
    ///
179
    /// The nonlinear parameters will be optimized across all the observations
180
    /// globally, but the best linear coefficients are calculated for each observation
181
    /// individually. Hence, the latter also become a matrix `$C$`, where the columns
182
    /// correspond to the linear coefficients of the observation in the same column.
183
    ///
184
    /// ```math
185
    /// \boldsymbol{C}=\left(\begin{matrix}
186
    ///  \vert &  & \vert \\
187
    ///  \vec{c}_1 &  \dots & \vec{c}_S \\
188
    ///  \vert &  & \vert \\
189
    /// \end{matrix}\right)
190
    /// ```
191
    ///
192
    /// The (column) vector of linear coefficients `$\vec{c}_j$` is for the observation
193
    /// `$\vec{y}_j$` in the same column.
194
    pub fn mrhs(model: Model) -> Self {
4✔
195
        Self {
196
            Y: None,
197
            separable_model: model,
198
            epsilon: None,
199
            weights: Weights::default(),
4✔
200
            phantom: Default::default(),
4✔
201
        }
202
    }
203
}
204

205
impl<Model> SeparableProblemBuilder<Model, MultiRhs>
206
where
207
    Model::ScalarType: Scalar + ComplexField + Zero + Copy,
208
    <Model::ScalarType as ComplexField>::RealField: Float,
209
    <<Model as SeparableNonlinearModel>::ScalarType as ComplexField>::RealField:
210
        Mul<Model::ScalarType, Output = Model::ScalarType> + Float,
211
    Model: SeparableNonlinearModel,
212
{
213
    /// **Mandatory**: Set the data which we want to fit: This is either a single vector
214
    /// `$\vec{y}=\vec{y}(\vec{x})$` or a matrix `$\boldsymbol{Y}$` of multiple
215
    /// vectors. In the former case this corresponds to fitting a single right hand side,
216
    /// in the latter case, this corresponds to global fitting of a problem with
217
    /// multiple right hand sides.
218
    /// The length of `$\vec{x}$` and the number of _rows_ in the data must
219
    /// be the same.
220
    pub fn observations(self, observed: OMatrix<Model::ScalarType, Dyn, Dyn>) -> Self {
4✔
221
        Self {
222
            Y: Some(observed),
4✔
223
            ..self
224
        }
225
    }
226
}
227

228
impl<Model, Rhs: RhsType> SeparableProblemBuilder<Model, Rhs>
229
where
230
    Model::ScalarType: Scalar + ComplexField + Zero + Copy,
231
    <Model::ScalarType as ComplexField>::RealField: Float,
232
    <<Model as SeparableNonlinearModel>::ScalarType as ComplexField>::RealField:
233
        Mul<Model::ScalarType, Output = Model::ScalarType> + Float,
234
    Model: SeparableNonlinearModel,
235
{
236
    /// **Optional** This value is relevant for the solver, because it uses singular value decomposition
237
    /// internally. This method sets a value `\epsilon` for which smaller (i.e. absolute - wise) singular
238
    /// values are considered zero. In essence this gives a truncation of the SVD. This might be
239
    /// helpful if two basis functions become linear dependent when the nonlinear model parameters
240
    /// align in an unfortunate way. In this case a higher epsilon might increase the robustness
241
    /// of the fitting process.
242
    ///
243
    /// If this value is not given, it will be set to machine epsilon.
244
    ///
245
    /// The given epsilon is automatically converted to a non-negative number.
246
    pub fn epsilon(self, eps: <Model::ScalarType as ComplexField>::RealField) -> Self {
1✔
247
        Self {
248
            epsilon: Some(<_ as Float>::abs(eps)),
1✔
249
            ..self
250
        }
251
    }
252

253
    /// **Optional** Add diagonal weights to the problem (meaning data points are statistically
254
    /// independent). If this is not given, the problem is unweighted, i.e. each data point has
255
    /// unit weight.
256
    ///
257
    /// **Note** The weighted residual is calculated as `$||W(\vec{y}-\vec{f}(\vec{\alpha})||^2$`, so
258
    /// to make weights that have a statistical meaning, the diagonal elements of the weight matrix should be
259
    /// set to `w_{jj} = 1/\sigma_j` where `$\sigma_j$` is the (estimated) standard deviation associated with
260
    /// data point `$y_j$`.
261
    pub fn weights(self, weights: OVector<Model::ScalarType, Dyn>) -> Self {
7✔
262
        Self {
263
            weights: Weights::diagonal(weights),
7✔
264
            ..self
265
        }
266
    }
267

268
    /// build the least squares problem from the builder.
269
    /// # Prerequisites
270
    /// * All mandatory parameters have been set (see individual builder methods for details)
271
    /// * `$\vec{x}$` and `$\vec{y}$` have the same number of elements
272
    /// * `$\vec{x}$` and `$\vec{y}$` have a nonzero number of elements
273
    /// * the length of the initial guesses vector is the same as the number of model parameters
274
    /// # Returns
275
    /// If all prerequisites are fulfilled, returns a [SeparableProblem](super::SeparableProblem) with the given
276
    /// content and the parameters set to the initial guess. Otherwise returns an error variant.
277
    #[allow(non_snake_case)]
278
    pub fn build(self) -> Result<SeparableProblem<Model, Rhs>, SeparableProblemBuilderError> {
27✔
279
        // and assign the defaults to the values we don't have
280
        let Y = self.Y.ok_or(SeparableProblemBuilderError::YDataMissing)?;
108✔
281
        let model = self.separable_model;
×
282
        let epsilon = self.epsilon.unwrap_or_else(Float::epsilon);
×
283
        let weights = self.weights;
×
284

285
        // now do some sanity checks for the values and return
286
        // an error if they do not pass the test
287
        let x_len: usize = model.output_len();
×
288
        if x_len == 0 || Y.is_empty() {
26✔
289
            return Err(SeparableProblemBuilderError::ZeroLengthVector);
1✔
290
        }
291

UNCOV
292
        if x_len != Y.nrows() {
×
293
            return Err(SeparableProblemBuilderError::InvalidLengthOfData {
1✔
294
                x_length: x_len,
2✔
295
                y_length: Y.nrows(),
1✔
296
            });
297
        }
298

UNCOV
299
        if !weights.is_size_correct_for_data_length(Y.nrows()) {
×
300
            //check that weights have correct length if they were given
301
            return Err(SeparableProblemBuilderError::InvalidLengthOfWeights);
1✔
302
        }
303

304
        //now that we have valid inputs, construct the levmar problem
305
        // 1) create weighted data
306
        #[allow(non_snake_case)]
307
        let Y_w = &weights * Y;
46✔
308

309
        let params = model.params();
69✔
310
        // 2) initialize the levmar problem. Some field values are dummy initialized
311
        // (like the SVD) because they are calculated in step 3 as part of set_params
312
        let mut problem = SeparableProblem {
313
            // these parameters all come from the builder
314
            Y_w,
315
            model,
316
            svd_epsilon: epsilon,
317
            cached: None,
318
            weights,
319
            phantom: Default::default(),
23✔
320
        };
321
        problem.set_params(&params);
69✔
322

323
        Ok(problem)
23✔
324
    }
325
}
326
// make available for testing and doc tests
327
#[cfg(any(test, doctest))]
328
mod test;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc