1use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum RillError {
12 #[error("dimension mismatch: expected {expected}, got {actual}")]
14 DimensionMismatch {
15 expected: usize,
17 actual: usize,
19 },
20
21 #[error("empty features are not allowed")]
23 EmptyFeatures,
24
25 #[error("invalid window size: must be greater than zero")]
27 InvalidWindowSize,
28
29 #[error("invalid learning rate: {0} (must be finite and > 0)")]
31 InvalidLearningRate(f64),
32
33 #[error("invalid parameter `{name}`: {value}")]
35 InvalidParameter {
36 name: &'static str,
38 value: f64,
40 },
41
42 #[error("non-finite value for `{field}`: {value}")]
44 NonFiniteValue {
45 field: &'static str,
47 value: f64,
49 },
50
51 #[error("invalid probability: {0} (must be in (0, 1))")]
53 InvalidProbability(f64),
54
55 #[error("insufficient data to compute the requested quantity")]
57 InsufficientData,
58
59 #[error("incompatible state version: expected {expected}, got {actual}")]
61 IncompatibleStateVersion {
62 expected: u32,
64 actual: u32,
66 },
67
68 #[error("sparse features must be sorted by FeatureId")]
70 UnsortedFeatureIds,
71
72 #[error("duplicate feature id: {0}")]
74 DuplicateFeatureId(u64),
75
76 #[error("invalid hash dimension: {0} (must be > 0)")]
78 InvalidHashDimension(usize),
79
80 #[error("unknown category: {0}")]
82 UnknownCategory(String),
83
84 #[error("missing value (NaN) at index {index}")]
86 MissingValue {
87 index: usize,
89 },
90
91 #[error("invalid capacity: {0} (must be greater than zero)")]
93 InvalidCapacity(usize),
94
95 #[error("invalid significance level: {0} (must be in (0, 1))")]
97 InvalidSignificanceLevel(f64),
98
99 #[error("invalid arm count: {0} (must be greater than zero)")]
101 InvalidArmCount(usize),
102
103 #[error("invalid epsilon: {0} (must be in [0, 1])")]
105 InvalidEpsilon(f64),
106
107 #[error("invalid arm index: {actual} (must be < {expected})")]
109 InvalidArm {
110 expected: usize,
112 actual: usize,
114 },
115
116 #[error("invalid reward: {0} (must be finite and in the valid range)")]
118 InvalidReward(f64),
119
120 #[error("invalid feature count: {0} (must be greater than zero)")]
122 InvalidFeatureCount(usize),
123
124 #[error("invalid model state: {0}")]
126 InvalidState(String),
127}
128
129pub(crate) fn ensure_finite(field: &'static str, value: f64) -> Result<(), RillError> {
131 if value.is_finite() {
132 Ok(())
133 } else {
134 Err(RillError::NonFiniteValue { field, value })
135 }
136}
137
138pub(crate) fn checked_finite_add(
140 current: f64,
141 delta: f64,
142 field: &'static str,
143) -> Result<f64, RillError> {
144 let value = current + delta;
145 ensure_finite(field, value)?;
146 Ok(value)
147}
148
149pub(crate) fn checked_increment(value: u64, field: &'static str) -> Result<u64, RillError> {
151 value
152 .checked_add(1)
153 .ok_or_else(|| RillError::InvalidState(format!("{field} counter overflow")))
154}
155
156pub(crate) fn validate_features(expected: usize, features: &[f64]) -> Result<(), RillError> {
158 if features.is_empty() {
159 return Err(RillError::EmptyFeatures);
160 }
161 if features.len() != expected {
162 return Err(RillError::DimensionMismatch {
163 expected,
164 actual: features.len(),
165 });
166 }
167 for (i, &v) in features.iter().enumerate() {
168 if !v.is_finite() {
169 return Err(RillError::NonFiniteValue {
170 field: "feature",
171 value: features[i],
172 });
173 }
174 }
175 Ok(())
176}
177
178pub(crate) fn ensure_finite_target(value: f64) -> Result<(), RillError> {
180 ensure_finite("target", value)
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn dimension_mismatch_display() {
189 let e = RillError::DimensionMismatch {
190 expected: 3,
191 actual: 2,
192 };
193 assert!(format!("{e}").contains("expected 3"));
194 assert!(format!("{e}").contains("got 2"));
195 }
196
197 #[test]
198 fn ensure_finite_passes_for_normal_values() {
199 assert!(ensure_finite("x", 1.5).is_ok());
200 assert!(ensure_finite("x", -1e10).is_ok());
201 assert!(ensure_finite("x", 0.0).is_ok());
202 }
203
204 #[test]
205 fn ensure_finite_rejects_nan_and_infinity() {
206 assert!(ensure_finite("x", f64::NAN).is_err());
207 assert!(ensure_finite("x", f64::INFINITY).is_err());
208 assert!(ensure_finite("x", f64::NEG_INFINITY).is_err());
209 }
210
211 #[test]
212 fn validate_features_checks_dimension() {
213 assert!(validate_features(3, &[1.0, 2.0, 3.0]).is_ok());
214 assert!(validate_features(3, &[1.0, 2.0]).is_err());
215 }
216
217 #[test]
218 fn validate_features_rejects_empty() {
219 assert!(matches!(
220 validate_features(0, &[]),
221 Err(RillError::EmptyFeatures)
222 ));
223 }
224
225 #[test]
226 fn validate_features_rejects_non_finite() {
227 assert!(validate_features(2, &[1.0, f64::NAN]).is_err());
228 assert!(validate_features(2, &[f64::INFINITY, 2.0]).is_err());
229 }
230
231 #[test]
232 fn invalid_arm_count_display() {
233 let e = RillError::InvalidArmCount(0);
234 assert!(format!("{e}").contains("invalid arm count"));
235 assert!(format!("{e}").contains("0"));
236 }
237
238 #[test]
239 fn invalid_epsilon_display() {
240 let e = RillError::InvalidEpsilon(-0.5);
241 assert!(format!("{e}").contains("invalid epsilon"));
242 assert!(format!("{e}").contains("-0.5"));
243 }
244
245 #[test]
246 fn invalid_arm_display() {
247 let e = RillError::InvalidArm {
248 expected: 3,
249 actual: 5,
250 };
251 assert!(format!("{e}").contains("5"));
252 assert!(format!("{e}").contains("3"));
253 }
254
255 #[test]
256 fn invalid_reward_display() {
257 let e = RillError::InvalidReward(f64::NAN);
258 assert!(format!("{e}").contains("invalid reward"));
259 }
260
261 #[test]
262 fn invalid_feature_count_display() {
263 let e = RillError::InvalidFeatureCount(0);
264 assert!(format!("{e}").contains("invalid feature count"));
265 assert!(format!("{e}").contains("0"));
266 }
267}