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

geo-ant / varpro / 4842747987

pending completion
4842747987

Pull #21

github

Unknown Committer
Unknown Commit Message
Pull Request #21: Feature/improved model api

349 of 349 new or added lines in 14 files covered. (100.0%)

592 of 669 relevant lines covered (88.49%)

966.01 hits per line

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

75.0
/src/model/test.rs
1
use crate::model::builder::SeparableModelBuilder;
2
use crate::model::errors::ModelError;
3
use crate::prelude::*;
4
use crate::test_helpers;
5
use assert_matches::assert_matches;
6
use nalgebra::DMatrix;
7
use nalgebra::DVector;
8
use nalgebra::Dyn;
9

10
// mock the separable model for later use in tests
11
// some exta manual labor because the mocker was having trouble with
12
// my trait
13
mockall::mock! {
441✔
14
    /// MockSeparableNonlinearModel that can be used
15
    /// in unit and integration tests inside this crate
16
    pub SeparableNonlinearModel {
378✔
17
       pub fn parameter_count(&self) -> Dyn;
378✔
18
       pub fn base_function_count(&self) -> usize;
378✔
19
       pub fn output_len(&self) -> usize;
378✔
20
       pub fn set_params(&mut self, parameters : DVector<f64>) -> Result<(),MockModelError>;
381✔
21
       pub fn params(&self) -> DVector<f64>;
378✔
22
       pub fn eval(
378✔
23
            &self,
378✔
24
        ) -> Result<DMatrix<f64>, MockModelError>;
378✔
25
       pub fn eval_partial_deriv(
378✔
26
            &self,
378✔
27
            derivative_index: usize,
378✔
28
        ) -> Result<DMatrix<f64>, MockModelError>;
378✔
29
    }
30

31
    impl Clone for SeparableNonlinearModel {
378✔
32
        fn clone(&self) -> Self;
378✔
33
    }
34
}
35

36
//derive a simple error using thiserror that can be
37
//converted from a string
38
#[derive(Debug, thiserror::Error)]
39
pub enum MockModelError {
40
    #[error("MockModelError: {}", 0)]
41
    Error(String),
42
}
43

44
impl<S> From<S> for MockModelError
45
where
46
    S: Into<String>,
47
{
48
    fn from(s: S) -> Self {
×
49
        MockModelError::Error(s.into())
×
50
    }
51
}
52

53
impl SeparableNonlinearModel for MockSeparableNonlinearModel {
54
    type Error = MockModelError;
55
    type ParameterDim = Dyn;
56
    type ModelDim = Dyn;
57
    type OutputDim = Dyn;
58
    type ScalarType = f64;
59

60
    fn parameter_count(&self) -> Dyn {
×
61
        self.parameter_count()
×
62
    }
63

64
    fn base_function_count(&self) -> Dyn {
×
65
        Dyn(self.base_function_count())
×
66
    }
67

68
    fn output_len(&self) -> Dyn {
6✔
69
        Dyn(self.output_len())
6✔
70
    }
71

72
    fn set_params(&mut self, parameters: DVector<f64>) -> Result<(), Self::Error> {
3✔
73
        self.set_params(parameters)
3✔
74
    }
75

76
    fn params(&self) -> DVector<f64> {
3✔
77
        self.params()
3✔
78
    }
79

80
    fn eval(&self) -> Result<DMatrix<f64>, Self::Error> {
3✔
81
        self.eval()
3✔
82
    }
83

84
    fn eval_partial_deriv(&self, derivative_index: usize) -> Result<DMatrix<f64>, Self::Error> {
×
85
        self.eval_partial_deriv(derivative_index)
×
86
    }
87
}
88

89
#[test]
90
fn model_gets_initialized_with_correct_parameter_names_and_count() {
91
    let model = test_helpers::get_double_exponential_model_with_constant_offset(
92
        DVector::zeros(10),
93
        vec![1., 2.],
94
    );
95
    assert_eq!(
96
        model.parameter_count(),
97
        Dyn(2),
98
        "Double exponential model has 2 parameters"
99
    );
100
    assert_eq!(
101
        model.parameters(),
102
        &["tau1", "tau2"],
103
        "Double exponential model has 2 parameters"
104
    );
105
}
106

107
#[test]
108
// test that the eval method produces correct results and gives a matrix that is ordered correctly
109
fn model_function_eval_produces_correct_result() {
110
    let tvec = DVector::from(vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]);
111
    let tau1 = 1.;
112
    let tau2 = 3.;
113

114
    let params = &[tau1, tau2];
115
    let model = test_helpers::get_double_exponential_model_with_constant_offset(
116
        tvec.clone(),
117
        params.to_vec(),
118
    );
119
    let eval_matrix = model.eval().expect("Model evaluation should not fail");
120

121
    let mut expected_eval_matrix = DMatrix::zeros(eval_matrix.nrows(), eval_matrix.ncols());
122

123
    expected_eval_matrix.set_column(0, &test_helpers::exp_decay(&tvec, tau2));
124
    expected_eval_matrix.set_column(1, &test_helpers::exp_decay(&tvec, tau1));
125
    expected_eval_matrix.set_column(2, &DVector::from_element(tvec.len(), 1.));
126

127
    assert_eq!(
128
        eval_matrix, expected_eval_matrix,
129
        "Model evaluation should produce the expected evaluation"
130
    );
131
}
132

133
#[test]
134
// test that when a base function does not return the same length result as its location argument,
135
// then the eval method fails
136
fn model_function_eval_fails_for_invalid_length_of_return_value_in_base_function() {
137
    let tvec = DVector::from(vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]);
138
    let model_with_bad_function = SeparableModelBuilder::<f64>::new(&["tau1", "tau2"])
139
        .function(&["tau2"], test_helpers::exp_decay)
140
        .partial_deriv("tau2", test_helpers::exp_decay_dtau)
141
        .function(&["tau1"], |_t: &DVector<_>, _tau| {
142
            DVector::from(vec![1., 3., 3., 7.])
143
        })
144
        .partial_deriv("tau1", test_helpers::exp_decay_dtau)
145
        .initial_parameters(vec![2., 4.])
146
        .independent_variable(tvec)
147
        .build()
148
        .expect("Model function creation should not fail, although function is bad");
149

150
    assert_matches!(model_with_bad_function.eval(),Err(ModelError::UnexpectedFunctionOutput{actual_length:4,..}),"Model must report an error when evaluated with a function that does not return the same length vector as independent variable");
151
}
152

153
#[test]
154
fn model_function_parameter_setting_fails_for_incorrect_number_of_parameters() {
155
    let tvec = DVector::from(vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]);
156
    let params = vec![1., 2.];
157
    let mut model = test_helpers::get_double_exponential_model_with_constant_offset(tvec, params);
158
    assert_eq!(
159
        model.parameter_count(),
160
        Dyn(2),
161
        "double exponential model should have 2 params"
162
    );
163
    // now deliberately provide a wrong number of parameters to
164
    // set_params and make sure this fails
165
    assert_matches!(
166
        model.set_params(DVector::from_vec(vec![1.])),
167
        Err(ModelError::IncorrectParameterCount { .. })
168
    );
169
}
170

171
#[test]
172
// test that the correct derivative matrices are produced for a valid model
173
fn model_derivative_evaluation_produces_correct_result() {
174
    let ones = |t: &DVector<_>| DVector::from_element(t.len(), 1.);
175

176
    let tvec = DVector::from(vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]);
177
    let tau = 3.;
178
    let omega = 1.5;
179
    let params = &[tau, omega];
180

181
    let model = SeparableModelBuilder::<f64>::new(&["tau", "omega"])
182
        .independent_variable(tvec.clone())
183
        .initial_parameters(params.to_vec())
184
        .function(&["tau"], test_helpers::exp_decay)
185
        .partial_deriv("tau", test_helpers::exp_decay_dtau)
186
        .invariant_function(ones)
187
        .function(&["omega", "tau"], test_helpers::sin_ometa_t_plus_phi) // so we make phi=tau of the model. Bit silly, but to produce a function that contributes to all partial derivs
188
        .partial_deriv("tau", test_helpers::sin_ometa_t_plus_phi_dphi)
189
        .partial_deriv("omega", test_helpers::sin_ometa_t_plus_phi_domega)
190
        .build()
191
        .expect("Valid model creation should not fail");
192

193
    let deriv_tau = model
194
        .eval_partial_deriv(0)
195
        .expect("Derivative eval must not fail");
196
    let deriv_omega = model
197
        .eval_partial_deriv(1)
198
        .expect("Derivative eval must not fail");
199

200
    // DERIVATIVE WITH RESPECT TO TAU
201
    // assert that the matrix has the correct dimenstions
202
    assert!(
203
        deriv_tau.ncols() == 3 && deriv_tau.nrows() == tvec.len(),
204
        "Deriv tau matrix does not have correct dimensions"
205
    );
206
    // now check the columns of the deriv with respect to tau
207
    // d/d(tau) exp(-t/tau)
208
    assert_eq!(
209
        DVector::from(deriv_tau.column(0)),
210
        test_helpers::exp_decay_dtau(&tvec, tau)
211
    );
212
    // d/d(tau) constant function = 0
213
    assert_eq!(
214
        DVector::from(deriv_tau.column(1)),
215
        DVector::from_element(tvec.len(), 0.)
216
    );
217
    // d/d(tau) sin(omega*t+tau)
218
    assert_eq!(
219
        DVector::from(deriv_tau.column(2)),
220
        test_helpers::sin_ometa_t_plus_phi_dphi(&tvec, omega, tau)
221
    );
222

223
    // DERIVATIVE WITH RESPECT TO OMEGA
224
    assert!(
225
        deriv_omega.ncols() == 3 && deriv_omega.nrows() == tvec.len(),
226
        "Deriv omega matrix does not have correct dimensions"
227
    );
228
    // d/d(omega) exp(-t/tau) = 0
229
    assert_eq!(
230
        DVector::from(deriv_omega.column(0)),
231
        DVector::from_element(tvec.len(), 0.)
232
    );
233
    // d/d(omega) constant function = 0
234
    assert_eq!(
235
        DVector::from(deriv_omega.column(1)),
236
        DVector::from_element(tvec.len(), 0.)
237
    );
238
    // d/d(omega) sin(omega*t+tau)
239
    assert_eq!(
240
        DVector::from(deriv_omega.column(2)),
241
        test_helpers::sin_ometa_t_plus_phi_domega(&tvec, omega, tau)
242
    );
243
}
244

245
#[test]
246
// check that the following error cases are covered and return the appropriate errors
247
// * derivative is evaluated and includes a function that does not give the same length vector
248
// back as the location (x) argument
249
// * a derivative for a parameter that is not in the model is requested (by index or by name)
250
// * the derivative is requested for a wrong number of parameter arguments
251
fn model_derivative_evaluation_error_cases() {
252
    let tvec = DVector::from(vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]);
253
    let model_with_bad_function = SeparableModelBuilder::<f64>::new(&["tau1", "tau2"])
254
        .independent_variable(tvec)
255
        .function(&["tau2"], test_helpers::exp_decay)
256
        .partial_deriv("tau2", test_helpers::exp_decay_dtau)
257
        .function(&["tau1"], test_helpers::exp_decay)
258
        .partial_deriv("tau1", |_t: &DVector<_>, _tau| {
259
            DVector::from(vec![1., 3., 3., 7.])
260
        })
261
        .initial_parameters(vec![2., 4.])
262
        .build()
263
        .expect("Model function creation should not fail, although function is bad");
264

265
    // deriv index 0 is tau1: this derivative is bad and should fail
266
    assert_matches!(
267
        model_with_bad_function.eval_partial_deriv(0),
268
        Err(ModelError::UnexpectedFunctionOutput { .. }),
269
        "Derivative for invalid function must fail with correct error"
270
    );
271

272
    // deriv index 0 is tau1: this derivative is good and should return an ok result
273
    assert!(
274
        model_with_bad_function.eval_partial_deriv(1).is_ok(),
275
        "Derivative eval for valid function should return Ok result"
276
    );
277

278
    // check an out of bounds index for the derivative
279
    assert_matches!(
280
        model_with_bad_function.eval_partial_deriv(100),
281
        Err(ModelError::DerivativeIndexOutOfBounds { .. }),
282
        "Derivative for invalid function must fail with correct error"
283
    );
284

285
    assert_matches!(
286
        model_with_bad_function.eval_partial_deriv(3),
287
        Err(ModelError::DerivativeIndexOutOfBounds { .. }),
288
        "Derivative for invalid function must fail with correct error"
289
    );
290
}
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