use thiserror::Error;
#[derive(Debug, Error)]
pub enum RillError {
#[error("dimension mismatch: expected {expected}, got {actual}")]
DimensionMismatch {
expected: usize,
actual: usize,
},
#[error("empty features are not allowed")]
EmptyFeatures,
#[error("invalid window size: must be greater than zero")]
InvalidWindowSize,
#[error("invalid learning rate: {0} (must be finite and > 0)")]
InvalidLearningRate(f64),
#[error("invalid parameter `{name}`: {value}")]
InvalidParameter {
name: &'static str,
value: f64,
},
#[error("non-finite value for `{field}`: {value}")]
NonFiniteValue {
field: &'static str,
value: f64,
},
#[error("invalid probability: {0} (must be in (0, 1))")]
InvalidProbability(f64),
#[error("insufficient data to compute the requested quantity")]
InsufficientData,
#[error("incompatible state version: expected {expected}, got {actual}")]
IncompatibleStateVersion {
expected: u32,
actual: u32,
},
#[error("sparse features must be sorted by FeatureId")]
UnsortedFeatureIds,
#[error("duplicate feature id: {0}")]
DuplicateFeatureId(u64),
#[error("invalid hash dimension: {0} (must be > 0)")]
InvalidHashDimension(usize),
#[error("unknown category: {0}")]
UnknownCategory(String),
#[error("missing value (NaN) at index {index}")]
MissingValue {
index: usize,
},
#[error("invalid capacity: {0} (must be greater than zero)")]
InvalidCapacity(usize),
#[error("invalid significance level: {0} (must be in (0, 1))")]
InvalidSignificanceLevel(f64),
#[error("invalid arm count: {0} (must be greater than zero)")]
InvalidArmCount(usize),
#[error("invalid epsilon: {0} (must be in [0, 1])")]
InvalidEpsilon(f64),
#[error("invalid arm index: {actual} (must be < {expected})")]
InvalidArm {
expected: usize,
actual: usize,
},
#[error("invalid reward: {0} (must be finite and in the valid range)")]
InvalidReward(f64),
#[error("invalid feature count: {0} (must be greater than zero)")]
InvalidFeatureCount(usize),
#[error("invalid model state: {0}")]
InvalidState(String),
}
pub(crate) fn ensure_finite(field: &'static str, value: f64) -> Result<(), RillError> {
if value.is_finite() {
Ok(())
} else {
Err(RillError::NonFiniteValue { field, value })
}
}
pub(crate) fn checked_finite_add(
current: f64,
delta: f64,
field: &'static str,
) -> Result<f64, RillError> {
let value = current + delta;
ensure_finite(field, value)?;
Ok(value)
}
pub(crate) fn checked_increment(value: u64, field: &'static str) -> Result<u64, RillError> {
value
.checked_add(1)
.ok_or_else(|| RillError::InvalidState(format!("{field} counter overflow")))
}
pub(crate) fn validate_features(expected: usize, features: &[f64]) -> Result<(), RillError> {
if features.is_empty() {
return Err(RillError::EmptyFeatures);
}
if features.len() != expected {
return Err(RillError::DimensionMismatch {
expected,
actual: features.len(),
});
}
for (i, &v) in features.iter().enumerate() {
if !v.is_finite() {
return Err(RillError::NonFiniteValue {
field: "feature",
value: features[i],
});
}
}
Ok(())
}
pub(crate) fn ensure_finite_target(value: f64) -> Result<(), RillError> {
ensure_finite("target", value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dimension_mismatch_display() {
let e = RillError::DimensionMismatch {
expected: 3,
actual: 2,
};
assert!(format!("{e}").contains("expected 3"));
assert!(format!("{e}").contains("got 2"));
}
#[test]
fn ensure_finite_passes_for_normal_values() {
assert!(ensure_finite("x", 1.5).is_ok());
assert!(ensure_finite("x", -1e10).is_ok());
assert!(ensure_finite("x", 0.0).is_ok());
}
#[test]
fn ensure_finite_rejects_nan_and_infinity() {
assert!(ensure_finite("x", f64::NAN).is_err());
assert!(ensure_finite("x", f64::INFINITY).is_err());
assert!(ensure_finite("x", f64::NEG_INFINITY).is_err());
}
#[test]
fn validate_features_checks_dimension() {
assert!(validate_features(3, &[1.0, 2.0, 3.0]).is_ok());
assert!(validate_features(3, &[1.0, 2.0]).is_err());
}
#[test]
fn validate_features_rejects_empty() {
assert!(matches!(
validate_features(0, &[]),
Err(RillError::EmptyFeatures)
));
}
#[test]
fn validate_features_rejects_non_finite() {
assert!(validate_features(2, &[1.0, f64::NAN]).is_err());
assert!(validate_features(2, &[f64::INFINITY, 2.0]).is_err());
}
#[test]
fn invalid_arm_count_display() {
let e = RillError::InvalidArmCount(0);
assert!(format!("{e}").contains("invalid arm count"));
assert!(format!("{e}").contains("0"));
}
#[test]
fn invalid_epsilon_display() {
let e = RillError::InvalidEpsilon(-0.5);
assert!(format!("{e}").contains("invalid epsilon"));
assert!(format!("{e}").contains("-0.5"));
}
#[test]
fn invalid_arm_display() {
let e = RillError::InvalidArm {
expected: 3,
actual: 5,
};
assert!(format!("{e}").contains("5"));
assert!(format!("{e}").contains("3"));
}
#[test]
fn invalid_reward_display() {
let e = RillError::InvalidReward(f64::NAN);
assert!(format!("{e}").contains("invalid reward"));
}
#[test]
fn invalid_feature_count_display() {
let e = RillError::InvalidFeatureCount(0);
assert!(format!("{e}").contains("invalid feature count"));
assert!(format!("{e}").contains("0"));
}
}