amberfork_align/
params.rs1use crate::diff::DiffParams;
16use crate::fork::ForkParams;
17use crate::nw::AlignParams;
18use std::fmt;
19
20#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum ParamError {
24 TauOutOfRange(f64),
26 ResyncKZero,
28 GapOpenNotPositive(f64),
30 GapExtNotPositive(f64),
32 GapExtExceedsOpen { gap_open: f64, gap_ext: f64 },
34}
35
36impl fmt::Display for ParamError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::TauOutOfRange(tau) => {
40 write!(f, "tau must be within [0, 1], got {tau}")
41 }
42 Self::ResyncKZero => {
43 write!(
44 f,
45 "resync_k must be at least 1 (0 would recover every blip)"
46 )
47 }
48 Self::GapOpenNotPositive(v) => {
49 write!(f, "gap_open must be positive and finite, got {v}")
50 }
51 Self::GapExtNotPositive(v) => {
52 write!(f, "gap_ext must be positive and finite, got {v}")
53 }
54 Self::GapExtExceedsOpen { gap_open, gap_ext } => write!(
55 f,
56 "gap_ext ({gap_ext}) must not exceed gap_open ({gap_open}): extending a gap \
57 costing more than opening one breaks the affine premise"
58 ),
59 }
60 }
61}
62
63impl std::error::Error for ParamError {}
64
65impl AlignParams {
66 pub fn validated(self) -> Result<Self, ParamError> {
71 if !self.gap_open.is_finite() || self.gap_open <= 0.0 {
72 return Err(ParamError::GapOpenNotPositive(self.gap_open));
73 }
74 if !self.gap_ext.is_finite() || self.gap_ext <= 0.0 {
75 return Err(ParamError::GapExtNotPositive(self.gap_ext));
76 }
77 if self.gap_ext > self.gap_open {
78 return Err(ParamError::GapExtExceedsOpen {
79 gap_open: self.gap_open,
80 gap_ext: self.gap_ext,
81 });
82 }
83 Ok(self)
84 }
85}
86
87impl ForkParams {
88 pub fn validated(self) -> Result<Self, ParamError> {
93 if !self.tau.is_finite() || !(0.0..=1.0).contains(&self.tau) {
94 return Err(ParamError::TauOutOfRange(self.tau));
95 }
96 if self.resync_k == 0 {
97 return Err(ParamError::ResyncKZero);
98 }
99 Ok(self)
100 }
101}
102
103impl DiffParams {
104 pub fn validated(self) -> Result<Self, ParamError> {
110 self.align.validated()?;
111 self.fork.validated()?;
112 Ok(self)
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn dev_calibrated_defaults_are_valid() {
122 assert_eq!(
123 DiffParams::default().validated(),
124 Ok(DiffParams::default()),
125 "the shipped defaults must satisfy their own invariants"
126 );
127 }
128
129 #[test]
130 fn tau_outside_the_cost_scale_is_rejected() {
131 for tau in [-0.1, 1.1, f64::NAN, f64::INFINITY] {
132 let params = ForkParams {
133 tau,
134 ..ForkParams::default()
135 };
136 match params.validated() {
137 Err(ParamError::TauOutOfRange(seen)) => {
138 assert!(seen.is_nan() == tau.is_nan() || seen == tau)
139 }
140 other => panic!("tau {tau} must be rejected, got {other:?}"),
141 }
142 }
143 }
144
145 #[test]
146 fn resync_k_zero_is_rejected() {
147 let params = ForkParams {
148 resync_k: 0,
149 ..ForkParams::default()
150 };
151 assert_eq!(params.validated(), Err(ParamError::ResyncKZero));
152 }
153
154 #[test]
155 fn non_positive_or_non_finite_gaps_are_rejected() {
156 for gap_open in [0.0, -0.5, f64::NAN] {
157 let params = AlignParams {
158 gap_open,
159 ..AlignParams::default()
160 };
161 assert!(
162 matches!(params.validated(), Err(ParamError::GapOpenNotPositive(_))),
163 "gap_open {gap_open} must be rejected"
164 );
165 }
166 for gap_ext in [0.0, -0.5, f64::INFINITY] {
167 let params = AlignParams {
168 gap_ext,
169 ..AlignParams::default()
170 };
171 assert!(
172 matches!(
173 params.validated(),
174 Err(ParamError::GapExtNotPositive(_) | ParamError::GapExtExceedsOpen { .. })
175 ),
176 "gap_ext {gap_ext} must be rejected"
177 );
178 }
179 }
180
181 #[test]
182 fn gap_ext_above_gap_open_breaks_the_affine_premise() {
183 let params = AlignParams {
184 gap_open: 0.3,
185 gap_ext: 0.6,
186 };
187 assert_eq!(
188 params.validated(),
189 Err(ParamError::GapExtExceedsOpen {
190 gap_open: 0.3,
191 gap_ext: 0.6
192 })
193 );
194 }
195
196 #[test]
197 fn diff_params_compose_both_validations() {
198 let bad_fork = DiffParams {
199 fork: ForkParams {
200 tau: 2.0,
201 ..ForkParams::default()
202 },
203 ..DiffParams::default()
204 };
205 assert!(matches!(
206 bad_fork.validated(),
207 Err(ParamError::TauOutOfRange(_))
208 ));
209
210 let bad_align = DiffParams {
211 align: AlignParams {
212 gap_open: -1.0,
213 ..AlignParams::default()
214 },
215 ..DiffParams::default()
216 };
217 assert!(matches!(
218 bad_align.validated(),
219 Err(ParamError::GapOpenNotPositive(_))
220 ));
221 }
222
223 #[test]
224 fn error_messages_name_the_offending_value() {
225 let msg = ParamError::TauOutOfRange(1.5).to_string();
226 assert!(msg.contains("1.5"), "got: {msg}");
227 let msg = ParamError::GapExtExceedsOpen {
228 gap_open: 0.3,
229 gap_ext: 0.6,
230 }
231 .to_string();
232 assert!(msg.contains("0.3") && msg.contains("0.6"), "got: {msg}");
233 }
234}