Skip to main content

koth_ms/config/
mod.rs

1//! Configuration. The `koth_ff` detection settings (`[file]`, `[hills]`,
2//! `[hills_ms2]`, `[features]`, `[scoring]`, `[output]` and [`KothConfig`]) live in
3//! `koth-core` and are re-exported here at their original paths; this module adds
4//! the `koth_align` configuration.
5
6use serde::{Deserialize, Serialize};
7
8use crate::alignment::AlignmentConfig;
9use crate::lfq::LfqConfig;
10
11mod align;
12
13pub use align::AlignOutputConfig;
14pub use koth_core::config::*;
15
16/// Top-level configuration for the `koth_align` binary.
17#[derive(Debug, Clone, Serialize, Deserialize, Default)]
18#[serde(default, deny_unknown_fields)]
19pub struct AlignConfig {
20    pub alignment: AlignmentConfig,
21    pub lfq: LfqConfig,
22    pub output: AlignOutputConfig,
23}
24
25impl AlignConfig {
26    pub fn from_toml(path: &std::path::Path) -> Result<Self, crate::error::KothError> {
27        let content = std::fs::read_to_string(path)?;
28        let cfg: Self = toml::from_str(&content)
29            .map_err(|e| crate::error::KothError::ConfigError(e.to_string()))?;
30        cfg.lfq.consensus.validate()?;
31        Ok(cfg)
32    }
33
34    pub fn to_toml_string(&self) -> Result<String, crate::error::KothError> {
35        toml::to_string(self).map_err(|e| crate::error::KothError::ConfigError(e.to_string()))
36    }
37}
38
39#[cfg(test)]
40mod config_parse_tests {
41    use super::AlignConfig;
42
43    fn parse_align(name: &str, toml_src: &str) {
44        if let Err(e) = toml::from_str::<AlignConfig>(toml_src) {
45            panic!("shipped koth_align config `{name}` failed to parse: {e}");
46        }
47    }
48
49    // These used to also parse eight tuned configs from `benchmark/config/`. The
50    // benchmark now lives in its own repository (pgarrett-scripps/koth-paper), and
51    // `include_str!` resolves at COMPILE time, so leaving them would not merely
52    // skip a test -- it would stop this crate building at all.
53    //
54    // The guard still covers everything this repository ships. The tuned configs
55    // are guarded on the other side by `#[serde(deny_unknown_fields)]`: a renamed
56    // or removed field makes koth_ff reject them the moment the benchmark runs,
57    // which is the only thing that runs them.
58
59    #[test]
60    fn shipped_align_configs_parse() {
61        parse_align(
62            "example_config_align.toml",
63            include_str!("../../../example_config_align.toml"),
64        );
65    }
66
67    #[test]
68    fn removed_consensus_gates_are_rejected_instead_of_ignored() {
69        for (key, value) in [
70            ("min_member_combined_score", "0.5"),
71            ("min_seed_combined_score", "0.75"),
72            ("min_group_size", "2"),
73            ("allow_replicated_weak_seeds", "true"),
74        ] {
75            let source = format!("[lfq.consensus]\n{key} = {value}\n");
76            let err = toml::from_str::<AlignConfig>(&source).unwrap_err();
77            assert!(err.to_string().contains(key));
78        }
79    }
80
81    #[test]
82    fn consensus_gate_rejects_invalid_values() {
83        let mut cfg = AlignConfig::default();
84        for value in [f64::NAN, f64::INFINITY, -0.01, 1.01] {
85            cfg.lfq.consensus.max_group_qvalue = value;
86            assert!(cfg.lfq.consensus.validate().is_err());
87        }
88        cfg.lfq.consensus.max_group_qvalue = 0.05;
89        cfg.lfq.consensus.rt_window_pct = -0.1;
90        assert!(cfg.lfq.consensus.validate().is_err());
91    }
92
93    #[test]
94    fn omitted_lfq_windows_use_release_defaults_without_changing_grouping() {
95        for text in [
96            "",
97            "[lfq]\n",
98            include_str!("../../../example_config_align.toml"),
99        ] {
100            let cfg: AlignConfig = toml::from_str(text).expect("valid alignment config");
101            assert_eq!(cfg.lfq.rt_window_pct, 0.005);
102            assert_eq!(cfg.lfq.im_tolerance, 0.015);
103            assert_eq!(cfg.lfq.consensus.rt_window_pct, 0.02);
104            assert_eq!(cfg.lfq.consensus.im_tolerance, 0.05);
105            assert_eq!(cfg.alignment.im_tolerance, 0.05);
106        }
107    }
108
109    #[test]
110    fn explicit_lfq_windows_override_defaults_without_changing_grouping() {
111        let cfg: AlignConfig =
112            toml::from_str("[lfq]\nrt_window_pct = 0.01\nim_tolerance = 0.025\n")
113                .expect("valid custom extraction windows");
114        assert_eq!(cfg.lfq.rt_window_pct, 0.01);
115        assert_eq!(cfg.lfq.im_tolerance, 0.025);
116        assert_eq!(cfg.lfq.consensus.rt_window_pct, 0.02);
117        assert_eq!(cfg.lfq.consensus.im_tolerance, 0.05);
118    }
119}