1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4#[serde(default, deny_unknown_fields)]
5pub struct HillsConfig {
6 pub min_scans: usize,
7 pub max_gap: usize,
8 pub split_hills: bool,
9 #[serde(default = "default_split_valley_ratio")]
13 pub split_valley_ratio: f64,
14 #[serde(default = "default_split_sigma_mult")]
19 pub split_sigma_mult: f64,
20 #[serde(default = "default_split_height_frac")]
24 pub split_height_frac: f64,
25 pub lfc_weight: f64,
30 #[serde(default)]
36 pub gap_fill_enabled: bool,
37 #[serde(default)]
40 pub smoothing_enabled: bool,
41 #[serde(default = "default_smoothing_window")]
44 pub smoothing_window: usize,
45 #[serde(default)]
53 pub filter_large_baseline_hills: bool,
54 #[serde(default = "default_large_hill_min_scans")]
57 pub large_hill_min_scans: usize,
58 #[serde(default = "default_large_hill_peak_factor")]
62 pub large_hill_peak_factor: f64,
63 #[serde(default)]
75 pub tic_norm_window: usize,
76 #[serde(default = "default_tic_norm_min_scale")]
77 pub tic_norm_min_scale: f64,
78 #[serde(default = "default_tic_norm_max_scale")]
79 pub tic_norm_max_scale: f64,
80 #[serde(default = "default_tic_norm_mode")]
91 pub tic_norm_mode: String,
92}
93
94fn default_smoothing_window() -> usize {
95 1
96}
97
98fn default_large_hill_min_scans() -> usize {
99 40
100}
101
102fn default_large_hill_peak_factor() -> f64 {
103 2.0
104}
105
106fn default_tic_norm_min_scale() -> f64 {
107 0.5
108}
109
110fn default_tic_norm_max_scale() -> f64 {
111 3.0
112}
113
114fn default_tic_norm_mode() -> String {
115 "median".to_string()
116}
117
118fn default_split_valley_ratio() -> f64 {
119 0.60
120}
121
122fn default_split_sigma_mult() -> f64 {
123 5.0
124}
125
126fn default_split_height_frac() -> f64 {
127 0.10
128}
129
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
143#[serde(default, deny_unknown_fields)]
144pub struct HillsMs2Overrides {
145 pub min_scans: Option<usize>,
149 pub max_gap: Option<usize>,
150 pub split_hills: Option<bool>,
151 pub split_valley_ratio: Option<f64>,
152 pub split_sigma_mult: Option<f64>,
153 pub split_height_frac: Option<f64>,
154 pub lfc_weight: Option<f64>,
155 pub gap_fill_enabled: Option<bool>,
156 pub smoothing_enabled: Option<bool>,
157 pub smoothing_window: Option<usize>,
158 pub filter_large_baseline_hills: Option<bool>,
159 pub large_hill_min_scans: Option<usize>,
160 pub large_hill_peak_factor: Option<f64>,
161 pub tic_norm_window: Option<usize>,
162 pub tic_norm_min_scale: Option<f64>,
163 pub tic_norm_max_scale: Option<f64>,
164 pub tic_norm_mode: Option<String>,
165}
166
167impl HillsMs2Overrides {
168 pub fn apply_to(&self, base: &HillsConfig) -> HillsConfig {
171 let mut c = base.clone();
172 if let Some(v) = self.min_scans {
173 c.min_scans = v;
174 }
175 if let Some(v) = self.max_gap {
176 c.max_gap = v;
177 }
178 if let Some(v) = self.split_hills {
179 c.split_hills = v;
180 }
181 if let Some(v) = self.split_valley_ratio {
182 c.split_valley_ratio = v;
183 }
184 if let Some(v) = self.split_sigma_mult {
185 c.split_sigma_mult = v;
186 }
187 if let Some(v) = self.split_height_frac {
188 c.split_height_frac = v;
189 }
190 if let Some(v) = self.lfc_weight {
191 c.lfc_weight = v;
192 }
193 if let Some(v) = self.gap_fill_enabled {
194 c.gap_fill_enabled = v;
195 }
196 if let Some(v) = self.smoothing_enabled {
197 c.smoothing_enabled = v;
198 }
199 if let Some(v) = self.smoothing_window {
200 c.smoothing_window = v;
201 }
202 if let Some(v) = self.filter_large_baseline_hills {
203 c.filter_large_baseline_hills = v;
204 }
205 if let Some(v) = self.large_hill_min_scans {
206 c.large_hill_min_scans = v;
207 }
208 if let Some(v) = self.large_hill_peak_factor {
209 c.large_hill_peak_factor = v;
210 }
211 if let Some(v) = self.tic_norm_window {
212 c.tic_norm_window = v;
213 }
214 if let Some(v) = self.tic_norm_min_scale {
215 c.tic_norm_min_scale = v;
216 }
217 if let Some(v) = self.tic_norm_max_scale {
218 c.tic_norm_max_scale = v;
219 }
220 if let Some(ref v) = self.tic_norm_mode {
221 c.tic_norm_mode = v.clone();
222 }
223 c
224 }
225}
226
227impl Default for HillsConfig {
228 fn default() -> Self {
229 Self {
230 min_scans: 3,
231 max_gap: 1,
232 split_hills: true,
233 split_valley_ratio: default_split_valley_ratio(),
234 split_sigma_mult: default_split_sigma_mult(),
235 split_height_frac: default_split_height_frac(),
236 lfc_weight: 0.3,
237 gap_fill_enabled: false,
238 smoothing_enabled: false,
239 smoothing_window: 1,
240 filter_large_baseline_hills: false,
241 large_hill_min_scans: default_large_hill_min_scans(),
242 large_hill_peak_factor: default_large_hill_peak_factor(),
243 tic_norm_window: 0,
244 tic_norm_min_scale: default_tic_norm_min_scale(),
245 tic_norm_max_scale: default_tic_norm_max_scale(),
246 tic_norm_mode: default_tic_norm_mode(),
247 }
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
261 fn every_override_field_is_applied() {
262 let base = HillsConfig {
263 min_scans: 5,
264 max_gap: 2,
265 split_hills: false,
266 split_valley_ratio: 0.5,
267 split_sigma_mult: 3.0,
268 split_height_frac: 0.2,
269 lfc_weight: 0.7,
270 gap_fill_enabled: true,
271 smoothing_enabled: true,
272 smoothing_window: 3,
273 filter_large_baseline_hills: true,
274 large_hill_min_scans: 30,
275 large_hill_peak_factor: 1.5,
276 tic_norm_window: 7,
277 tic_norm_min_scale: 0.4,
278 tic_norm_max_scale: 2.5,
279 tic_norm_mode: "tic".to_string(),
280 };
281 let ov = HillsMs2Overrides {
282 min_scans: Some(9),
283 max_gap: Some(4),
284 split_hills: Some(true),
285 split_valley_ratio: Some(0.8),
286 split_sigma_mult: Some(6.0),
287 split_height_frac: Some(0.3),
288 lfc_weight: Some(0.9),
289 gap_fill_enabled: Some(false),
290 smoothing_enabled: Some(false),
291 smoothing_window: Some(6),
292 filter_large_baseline_hills: Some(false),
293 large_hill_min_scans: Some(50),
294 large_hill_peak_factor: Some(3.0),
295 tic_norm_window: Some(11),
296 tic_norm_min_scale: Some(0.6),
297 tic_norm_max_scale: Some(4.0),
298 tic_norm_mode: Some("median".to_string()),
299 };
300 let got = ov.apply_to(&base);
301
302 assert_ne!(got.min_scans, base.min_scans);
303 assert_ne!(got.max_gap, base.max_gap);
304 assert_ne!(got.split_hills, base.split_hills);
305 assert_ne!(got.split_valley_ratio, base.split_valley_ratio);
306 assert_ne!(got.split_sigma_mult, base.split_sigma_mult);
307 assert_ne!(got.split_height_frac, base.split_height_frac);
308 assert_ne!(got.lfc_weight, base.lfc_weight);
309 assert_ne!(got.gap_fill_enabled, base.gap_fill_enabled);
310 assert_ne!(got.smoothing_enabled, base.smoothing_enabled);
311 assert_ne!(got.smoothing_window, base.smoothing_window);
312 assert_ne!(
313 got.filter_large_baseline_hills,
314 base.filter_large_baseline_hills
315 );
316 assert_ne!(got.large_hill_min_scans, base.large_hill_min_scans);
317 assert_ne!(got.large_hill_peak_factor, base.large_hill_peak_factor);
318 assert_ne!(got.tic_norm_window, base.tic_norm_window);
319 assert_ne!(got.tic_norm_min_scale, base.tic_norm_min_scale);
320 assert_ne!(got.tic_norm_max_scale, base.tic_norm_max_scale);
321 assert_ne!(got.tic_norm_mode, base.tic_norm_mode);
322
323 assert_eq!(got.min_scans, 9);
325 assert_eq!(got.tic_norm_mode, "median");
326 }
327
328 #[test]
331 fn none_override_keeps_base_value() {
332 let base = HillsConfig {
333 min_scans: 5,
334 ..HillsConfig::default()
335 };
336 let got = HillsMs2Overrides::default().apply_to(&base);
337 assert_eq!(got.min_scans, 5);
338 }
339}