pub mod curvature;
pub mod fenchel;
pub mod lapsum;
pub mod loss;
pub mod metrics;
pub mod sigmoid;
pub mod sinkhorn;
pub mod sorting_network;
pub mod topk;
use thiserror::Error;
pub use fenchel::{
entmax, entropy_bits, entropy_nats, softmax, softmax_with_temperature, sparsemax, Regularizer,
Shannon, SquaredL2, Tsallis,
};
#[allow(deprecated)]
pub use metrics::{compute_rank, hits_at_k, mean_rank, mrr, ndcg, ndcg_at_k, RankingMetrics};
pub use sigmoid::{sigmoid, sigmoid_derivative};
pub use sinkhorn::{sinkhorn_rank, sinkhorn_sort, SinkhornConfig};
pub use topk::{
differentiable_bottomk, differentiable_topk, sparse_topk, sparse_topk_matrix, topk_ce_loss,
topk_cross_entropy_loss,
};
pub use lapsum::{lapsum_permutation, lapsum_rank, lapsum_sort, lapsum_topk};
pub use sorting_network::{
bitonic_sort, odd_even_sort, ranks_from_permutation, DiffSortNet, NetworkType, RelaxDist,
};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("empty input")]
EmptyInput,
#[error("temperature must be positive: {0}")]
InvalidTemperature(f64),
#[error("weights must be positive")]
InvalidWeights,
#[error("length mismatch: {0} vs {1}")]
LengthMismatch(usize, usize),
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn pava(y: &[f64]) -> Vec<f64> {
if y.is_empty() {
return vec![];
}
let n = y.len();
let mut result = y.to_vec();
let mut blocks: Vec<(usize, f64, usize)> = Vec::with_capacity(n);
for (i, &val) in y.iter().enumerate() {
blocks.push((i, val, 1));
while blocks.len() > 1 {
let len = blocks.len();
let (_, sum1, cnt1) = blocks[len - 2];
let (_, sum2, cnt2) = blocks[len - 1];
let mean1 = sum1 / cnt1 as f64;
let mean2 = sum2 / cnt2 as f64;
if mean1 > mean2 {
blocks.pop();
let Some(last) = blocks.last_mut() else {
debug_assert!(false, "blocks unexpectedly empty after pop");
break;
};
last.1 += sum2;
last.2 += cnt2;
} else {
break;
}
}
}
for (start, sum, count) in blocks {
let mean = sum / count as f64;
for r in result.iter_mut().skip(start).take(count) {
*r = mean;
}
}
result
}
pub fn pava_weighted(y: &[f64], weights: &[f64]) -> Result<Vec<f64>> {
if y.is_empty() {
return Ok(vec![]);
}
if y.len() != weights.len() {
return Err(Error::LengthMismatch(y.len(), weights.len()));
}
if weights.iter().any(|&w| w <= 0.0) {
return Err(Error::InvalidWeights);
}
let n = y.len();
let mut result = y.to_vec();
let mut blocks: Vec<(usize, f64, f64)> = Vec::with_capacity(n);
for (i, (&val, &w)) in y.iter().zip(weights.iter()).enumerate() {
blocks.push((i, val * w, w));
while blocks.len() > 1 {
let len = blocks.len();
let (_, wsum1, w1) = blocks[len - 2];
let (_, wsum2, w2) = blocks[len - 1];
let mean1 = wsum1 / w1;
let mean2 = wsum2 / w2;
if mean1 > mean2 {
blocks.pop();
let Some(last) = blocks.last_mut() else {
debug_assert!(false, "blocks unexpectedly empty after pop");
break;
};
last.1 += wsum2;
last.2 += w2;
} else {
break;
}
}
}
for (block_idx, (start, wsum, total_w)) in blocks.iter().enumerate() {
let mean = wsum / total_w;
let end = if block_idx + 1 < blocks.len() {
blocks[block_idx + 1].0
} else {
n
};
for r in result.iter_mut().skip(*start).take(end - *start) {
*r = mean;
}
}
Ok(result)
}
pub fn soft_rank(x: &[f64], temperature: f64) -> Result<Vec<f64>> {
if x.is_empty() {
return Err(Error::EmptyInput);
}
if temperature <= 0.0 {
return Err(Error::InvalidTemperature(temperature));
}
let n = x.len();
let mut ranks = vec![1.0; n];
let inv_temp = 1.0 / temperature;
for i in 0..n {
for j in (i + 1)..n {
let diff = (x[j] - x[i]) * inv_temp;
let s = 1.0 / (1.0 + (-diff).exp());
ranks[i] += s;
ranks[j] += 1.0 - s;
}
}
Ok(ranks)
}
pub fn soft_sort(x: &[f64], temperature: f64) -> Result<Vec<f64>> {
sinkhorn::sinkhorn_sort(x, temperature)
}
pub fn fast_soft_sort(x: &[f64], temperature: f64) -> Vec<f64> {
if x.is_empty() {
return vec![];
}
let n = x.len();
let mut indexed: Vec<(usize, f64)> = x.iter().copied().enumerate().collect();
indexed.sort_by(|a, b| a.1.total_cmp(&b.1));
let (_indices, sorted): (Vec<usize>, Vec<f64>) = indexed.into_iter().unzip();
let rho: Vec<f64> = (1..=n).map(|i| i as f64 * temperature).collect();
let mut diff = Vec::with_capacity(n);
for i in 0..n {
diff.push(sorted[i] - rho[i]);
}
let v = pava(&diff);
let mut res = Vec::with_capacity(n);
for i in 0..n {
res.push(sorted[i] - v[i]);
}
res
}
#[deprecated(
since = "0.1.2",
note = "use rankops::rrf / rankops::rrf_multi instead"
)]
pub fn reciprocal_rank_fusion<T: std::hash::Hash + Eq + Clone>(
rankings: &[Vec<T>],
k: usize,
) -> Vec<(T, f64)> {
use std::collections::HashMap;
let mut scores = HashMap::new();
for ranking in rankings {
for (rank, id) in ranking.iter().enumerate() {
let score = 1.0 / (k as f64 + (rank + 1) as f64);
*scores.entry(id.clone()).or_insert(0.0) += score;
}
}
let mut fused: Vec<_> = scores.into_iter().collect();
fused.sort_by(|a, b| b.1.total_cmp(&a.1));
fused
}
#[deprecated(since = "0.1.2", note = "use pava instead")]
pub fn isotonic_l2(y: &[f64]) -> Vec<f64> {
pava(y)
}
pub fn soft_topk_indicator(x: &[f64], k: usize, temperature: f64) -> Result<Vec<f64>> {
let ranks = soft_rank(x, temperature)?;
let k_f = k as f64;
Ok(ranks
.iter()
.map(|&r| {
let z = (k_f + 0.5 - r) / temperature;
1.0 / (1.0 + (-z).exp())
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_pava_simple() {
let y = [3.0, 1.0, 2.0, 5.0, 4.0];
let result = pava(&y);
for i in 1..result.len() {
assert!(result[i] >= result[i - 1], "Not monotonic at {}", i);
}
assert_relative_eq!(result[0], result[1], epsilon = 1e-10);
assert_relative_eq!(result[1], result[2], epsilon = 1e-10);
assert_relative_eq!(result[3], result[4], epsilon = 1e-10);
}
#[test]
fn test_pava_already_monotonic() {
let y = [1.0, 2.0, 3.0, 4.0, 5.0];
let result = pava(&y);
assert_eq!(result, y);
}
#[test]
fn test_pava_reverse() {
let y = [5.0, 4.0, 3.0, 2.0, 1.0];
let result = pava(&y);
let mean = 3.0;
for &r in &result {
assert_relative_eq!(r, mean, epsilon = 1e-10);
}
}
#[test]
fn test_soft_rank_ordering() {
let x = [0.1, 0.5, 0.2, 0.9];
let ranks = soft_rank(&x, 0.01).unwrap();
assert!(ranks[3] < ranks[1]); assert!(ranks[1] < ranks[2]); assert!(ranks[2] < ranks[0]); }
#[test]
fn test_soft_sort_approximates_sort() {
let x = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
let sorted = soft_sort(&x, 0.1).unwrap();
for i in 1..sorted.len() {
assert!(
sorted[i] >= sorted[i - 1] - 0.5,
"Not approximately sorted at {}: {} < {}",
i,
sorted[i],
sorted[i - 1]
);
}
}
#[test]
fn test_soft_topk() {
let x = [0.1, 0.9, 0.5, 0.8, 0.2];
let indicator = soft_topk_indicator(&x, 2, 0.01).unwrap();
assert!(indicator[1] > 0.5);
assert!(indicator[3] > 0.5);
assert!(indicator[0] < 0.5);
assert!(indicator[2] < 0.5);
assert!(indicator[4] < 0.5);
}
#[test]
fn test_empty_input() {
assert!(pava(&[]).is_empty());
assert!(soft_rank(&[], 0.1).is_err());
assert!(soft_sort(&[], 0.1).is_err());
}
#[test]
fn test_invalid_temperature() {
let x = [1.0, 2.0];
assert!(soft_rank(&x, 0.0).is_err());
assert!(soft_rank(&x, -1.0).is_err());
}
}