candding 0.1.0

Pure candle embeddings for Rust with no ONNX: dense, sparse, multi-vector and rerank models today, image models on the roadmap
Documentation
use std::cmp::Reverse;

use super::*;

#[test]
fn value_returns_the_number_it_was_built_from() {
  let score = RerankScore::new(3.5);
  assert_eq!(score.value(), 3.5);
}

#[test]
fn sorting_descending_ranks_the_best_match_first() {
  let mut scores = [
    RerankScore::new(-11.25),
    RerankScore::new(8.85),
    RerankScore::new(0.0),
  ];
  scores.sort_by_key(|s| Reverse(*s));
  assert_eq!(
    scores.iter().map(RerankScore::value).collect::<Vec<_>>(),
    vec![8.85, 0.0, -11.25]
  );
}

#[test]
fn a_nan_score_compares_and_sorts_without_panicking() {
  let nan = RerankScore::new(f32::NAN);
  let ordinary = RerankScore::new(1.0);
  let _ = nan.cmp(&ordinary);
  let mut scores = [ordinary, nan, RerankScore::new(-1.0)];
  scores.sort();
  assert!(scores[0].value().is_nan());
  assert_eq!(scores[1].value(), -1.0);
  assert_eq!(scores[2].value(), 1.0);
}

/// Sign bit clear is `f32::NAN` and Apple Silicon's `0.0 / 0.0`; set is x86's default `NaN`.
fn nan(sign_bit: bool) -> f32 {
  f32::from_bits(if sign_bit { 0xffc0_0000 } else { 0x7fc0_0000 })
}

/// An order that followed `f32::total_cmp` for a `NaN` would put the positive one above `+inf`.
#[test]
fn a_nan_of_either_sign_sorts_below_every_number() {
  for sign_bit in [false, true] {
    let value = nan(sign_bit);
    assert_eq!(value.is_sign_negative(), sign_bit);
    let nan = RerankScore::new(value);
    for number in [f32::NEG_INFINITY, -11.25, -0.0, 0.0, 8.85, f32::INFINITY] {
      let number = RerankScore::new(number);
      assert_eq!(
        nan.cmp(&number),
        Ordering::Less,
        "{value:?} against {number:?}"
      );
      assert_eq!(
        number.cmp(&nan),
        Ordering::Greater,
        "{number:?} against {value:?}"
      );
    }
  }
  assert_eq!(
    RerankScore::new(nan(false)).cmp(&RerankScore::new(nan(true))),
    Ordering::Equal
  );
}

#[test]
fn a_best_first_sort_ranks_a_nan_of_either_sign_last() {
  let values = [8.85, nan(false), -11.25, nan(true), f32::NEG_INFINITY];
  let mut ranked: Vec<(usize, RerankScore)> = values
    .iter()
    .copied()
    .map(RerankScore::new)
    .enumerate()
    .collect();
  ranked.sort_by_key(|&(_, score)| Reverse(score));
  assert_eq!(
    ranked.iter().map(|&(index, _)| index).collect::<Vec<_>>(),
    vec![0, 2, 4, 1, 3]
  );
}

#[test]
fn score_activation_serializes_snake_case() {
  assert_eq!(
    serde_json::to_string(&ScoreActivation::Identity).unwrap(),
    "\"identity\""
  );
  assert_eq!(
    serde_json::from_str::<ScoreActivation>("\"sigmoid\"").unwrap(),
    ScoreActivation::Sigmoid
  );
}

#[test]
fn identity_returns_the_raw_value_unchanged() {
  assert_eq!(ScoreActivation::Identity.apply(8.845_856), 8.845_856);
  assert_eq!(ScoreActivation::Identity.apply(-11.245_562), -11.245_562);
}

/// Checked against Python's `1 / (1 + math.exp(-8.8458557))`, not the expression the method uses.
#[test]
fn sigmoid_bounds_the_value_to_zero_one_and_matches_the_textbook_formula() {
  assert_eq!(ScoreActivation::Sigmoid.apply(0.0), 0.5);
  let relevant = ScoreActivation::Sigmoid.apply(8.845_856);
  assert!((relevant - 0.999_856_04).abs() < 1e-6, "{relevant}");
  let irrelevant = ScoreActivation::Sigmoid.apply(-11.245_562);
  assert!((irrelevant - 0.000_013_065).abs() < 1e-7, "{irrelevant}");
  for raw in [-1000.0f32, 1000.0] {
    let bounded = ScoreActivation::Sigmoid.apply(raw);
    assert!((0.0..=1.0).contains(&bounded), "{raw} -> {bounded}");
  }
}