use serde::{Deserialize, Serialize};
use crate::error::{BodhError, Result, validate_finite, validate_positive};
#[inline]
#[must_use = "returns the perceived intensity without side effects"]
pub fn weber_fechner(stimulus_intensity: f64, reference: f64, k: f64) -> Result<f64> {
validate_positive(stimulus_intensity, "stimulus_intensity")?;
validate_positive(reference, "reference")?;
validate_finite(k, "k")?;
Ok(k * (stimulus_intensity / reference).ln())
}
#[inline]
#[must_use = "returns the Weber fraction without side effects"]
pub fn weber_fraction(delta_intensity: f64, reference_intensity: f64) -> Result<f64> {
validate_finite(delta_intensity, "delta_intensity")?;
validate_positive(reference_intensity, "reference_intensity")?;
Ok(delta_intensity / reference_intensity)
}
#[inline]
#[must_use = "returns the sensation magnitude without side effects"]
pub fn stevens_power_law(stimulus: f64, k: f64, exponent: f64) -> Result<f64> {
crate::error::validate_non_negative(stimulus, "stimulus")?;
validate_finite(k, "k")?;
validate_finite(exponent, "exponent")?;
Ok(k * stimulus.powf(exponent))
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum StevensExponent {
Brightness,
Loudness,
Vibration,
ElectricShock,
Heaviness,
Temperature,
}
impl StevensExponent {
#[inline]
#[must_use]
pub fn value(self) -> f64 {
match self {
Self::Brightness => 0.33,
Self::Loudness => 0.67,
Self::Vibration => 0.95,
Self::ElectricShock => 3.5,
Self::Heaviness => 1.45,
Self::Temperature => 1.6,
}
}
}
#[inline]
#[must_use = "returns the index of difficulty in bits without side effects"]
pub fn fitts_law(distance: f64, width: f64) -> Result<f64> {
validate_positive(distance, "distance")?;
validate_positive(width, "width")?;
Ok((2.0 * distance / width).log2())
}
#[inline]
#[must_use = "returns the index of difficulty in bits without side effects"]
pub fn fitts_law_shannon(distance: f64, width: f64) -> Result<f64> {
crate::error::validate_non_negative(distance, "distance")?;
validate_positive(width, "width")?;
Ok((distance / width + 1.0).log2())
}
#[inline]
#[must_use = "returns the movement time without side effects"]
pub fn fitts_law_full(distance: f64, width: f64, a: f64, b: f64) -> Result<f64> {
let id = fitts_law(distance, width)?;
validate_finite(a, "a")?;
validate_finite(b, "b")?;
Ok(a + b * id)
}
#[inline]
#[must_use = "returns the movement time without side effects"]
pub fn fitts_law_shannon_full(distance: f64, width: f64, a: f64, b: f64) -> Result<f64> {
let id = fitts_law_shannon(distance, width)?;
validate_finite(a, "a")?;
validate_finite(b, "b")?;
Ok(a + b * id)
}
#[inline]
#[must_use = "returns the decision time without side effects"]
pub fn hicks_law(choices: usize, b: f64) -> Result<f64> {
if choices == 0 {
return Err(BodhError::InvalidParameter(
"choices must be at least 1".into(),
));
}
validate_finite(b, "b")?;
Ok(b * (choices as f64).log2())
}
#[inline]
#[must_use = "returns the decision time without side effects"]
pub fn hicks_law_full(choices: usize, a: f64, b: f64) -> Result<f64> {
validate_finite(a, "a")?;
let info_component = hicks_law(choices, b)?;
Ok(a + info_component)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_weber_fechner_basic() {
let p = weber_fechner(100.0, 100.0, 1.0).unwrap();
assert!((p - 0.0).abs() < 1e-10);
}
#[test]
fn test_weber_fechner_k1_double() {
let p = weber_fechner(200.0, 100.0, 1.0).unwrap();
assert!((p - 2.0_f64.ln()).abs() < 1e-10);
}
#[test]
fn test_weber_fechner_invalid() {
assert!(weber_fechner(0.0, 100.0, 1.0).is_err());
assert!(weber_fechner(100.0, 0.0, 1.0).is_err());
}
#[test]
fn test_weber_fraction() {
let w = weber_fraction(10.0, 100.0).unwrap();
assert!((w - 0.1).abs() < 1e-10);
}
#[test]
fn test_stevens_power_law_brightness() {
let s1 = stevens_power_law(100.0, 1.0, 0.33).unwrap();
let s2 = stevens_power_law(200.0, 1.0, 0.33).unwrap();
assert!(s2 > s1);
assert!(s2 < 2.0 * s1); }
#[test]
fn test_stevens_power_law_linear() {
let s = stevens_power_law(50.0, 2.0, 1.0).unwrap();
assert!((s - 100.0).abs() < 1e-10);
}
#[test]
fn test_stevens_exponent_values() {
assert!((StevensExponent::Brightness.value() - 0.33).abs() < 1e-10);
assert!((StevensExponent::Loudness.value() - 0.67).abs() < 1e-10);
assert!((StevensExponent::ElectricShock.value() - 3.5).abs() < 1e-10);
}
#[test]
fn test_fitts_law_basic() {
let id = fitts_law(256.0, 4.0).unwrap();
assert!((id - 7.0).abs() < 1e-10);
}
#[test]
fn test_fitts_law_easy_target() {
let id = fitts_law(10.0, 10.0).unwrap();
assert!((id - 1.0).abs() < 1e-10);
}
#[test]
fn test_fitts_law_invalid() {
assert!(fitts_law(0.0, 4.0).is_err());
assert!(fitts_law(256.0, 0.0).is_err());
}
#[test]
fn test_hicks_law_basic() {
let rt = hicks_law(8, 1.0).unwrap();
assert!((rt - 3.0).abs() < 1e-10);
}
#[test]
fn test_hicks_law_with_intercept() {
let rt = hicks_law_full(8, 0.2, 0.1).unwrap();
assert!((rt - 0.5).abs() < 1e-10);
}
#[test]
fn test_hicks_law_single_choice() {
let rt = hicks_law(1, 1.0).unwrap();
assert!((rt - 0.0).abs() < 1e-10);
}
#[test]
fn test_hicks_law_zero_choices() {
assert!(hicks_law(0, 1.0).is_err());
}
#[test]
fn test_fitts_law_full() {
let mt = fitts_law_full(256.0, 4.0, 0.1, 0.05).unwrap();
assert!((mt - 0.45).abs() < 1e-10);
}
#[test]
fn test_hicks_law_full_basic() {
let rt = hicks_law_full(4, 0.0, 1.0).unwrap();
assert!((rt - 2.0).abs() < 1e-10); }
#[test]
fn test_fitts_law_shannon_basic() {
let id = fitts_law_shannon(256.0, 4.0).unwrap();
assert!((id - 65.0_f64.log2()).abs() < 1e-10);
}
#[test]
fn test_fitts_law_shannon_zero_distance() {
let id = fitts_law_shannon(0.0, 4.0).unwrap();
assert!((id - 0.0).abs() < 1e-10);
}
#[test]
fn test_fitts_law_shannon_full() {
let mt = fitts_law_shannon_full(256.0, 4.0, 0.1, 0.05).unwrap();
let expected = 0.1 + 0.05 * 65.0_f64.log2();
assert!((mt - expected).abs() < 1e-10);
}
#[test]
fn test_fitts_law_shannon_invalid() {
assert!(fitts_law_shannon(-1.0, 4.0).is_err());
assert!(fitts_law_shannon(10.0, 0.0).is_err());
}
#[test]
fn test_stevens_exponent_serde_roundtrip() {
let exp = StevensExponent::Loudness;
let json = serde_json::to_string(&exp).unwrap();
let back: StevensExponent = serde_json::from_str(&json).unwrap();
assert_eq!(exp, back);
}
}