1use crate::DeterministicScore;
4use std::fmt;
5use std::num::NonZeroUsize;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum ScoreError {
13 LengthMismatch {
15 expected_desc: &'static str,
17 first_len: usize,
19 second_len: usize,
21 },
22 NonFiniteWeight {
24 index: usize,
26 },
27 NonFiniteDistance,
29 InvalidDistanceRange {
31 metric_name: &'static str,
33 dist_bits: u32,
35 },
36 UnsupportedMetric,
38}
39
40impl fmt::Display for ScoreError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 ScoreError::LengthMismatch {
44 expected_desc,
45 first_len,
46 second_len,
47 } => write!(
48 f,
49 "{expected_desc}: first has {first_len} elements, second has {second_len}"
50 ),
51 ScoreError::NonFiniteWeight { index } => {
52 write!(f, "weight at index {index} must be finite")
53 }
54 ScoreError::NonFiniteDistance => {
55 write!(f, "distance must be finite (not NaN or infinity)")
56 }
57 ScoreError::InvalidDistanceRange {
58 metric_name,
59 dist_bits,
60 } => write!(
61 f,
62 "distance value (bits=0x{dist_bits:08x}) is out of valid range for metric {metric_name}"
63 ),
64 ScoreError::UnsupportedMetric => {
65 write!(f, "unsupported distance metric")
66 }
67 }
68 }
69}
70
71impl std::error::Error for ScoreError {}
72
73#[inline]
75pub fn sum_scores(scores: &[DeterministicScore]) -> DeterministicScore {
76 if scores.is_empty() {
77 return DeterministicScore::ZERO;
78 }
79 let sum: i128 = scores.iter().map(|s| s.to_raw() as i128).sum();
80 DeterministicScore::from_raw(sum.clamp(
81 DeterministicScore::NEG_INF.to_raw() as i128,
82 i64::MAX as i128,
83 ) as i64)
84}
85
86#[inline]
88pub fn avg_scores(scores: &[DeterministicScore]) -> DeterministicScore {
89 if scores.is_empty() {
90 return DeterministicScore::ZERO;
91 }
92 let sum: i128 = scores.iter().map(|s| s.to_raw() as i128).sum();
93 let mean = sum / scores.len() as i128;
94 DeterministicScore::from_raw(mean.clamp(
95 DeterministicScore::NEG_INF.to_raw() as i128,
96 i64::MAX as i128,
97 ) as i64)
98}
99
100#[inline]
102pub fn avg_scores_checked(scores: &[DeterministicScore]) -> (DeterministicScore, bool) {
103 if scores.is_empty() {
104 return (DeterministicScore::ZERO, false);
105 }
106 const SATURATION_THRESHOLD: i128 = (i64::MAX as i128) * 9 / 10;
107 let sum: i128 = scores.iter().map(|s| s.to_raw() as i128).sum();
108 let mean = sum / scores.len() as i128;
109 let abs_mass: i128 = scores
111 .iter()
112 .map(|s| (s.to_raw() as i128).unsigned_abs() as i128)
113 .sum();
114 let near_saturation =
115 abs_mass > SATURATION_THRESHOLD || mean.unsigned_abs() as i128 > SATURATION_THRESHOLD;
116 let result = DeterministicScore::from_raw(mean.clamp(
117 DeterministicScore::NEG_INF.to_raw() as i128,
118 i64::MAX as i128,
119 ) as i64);
120 (result, near_saturation)
121}
122
123#[inline]
125pub fn max_score(scores: &[DeterministicScore]) -> DeterministicScore {
126 scores
127 .iter()
128 .copied()
129 .max()
130 .unwrap_or(DeterministicScore::NEG_INF)
131}
132
133#[inline]
135pub fn min_score(scores: &[DeterministicScore]) -> DeterministicScore {
136 scores
137 .iter()
138 .copied()
139 .min()
140 .unwrap_or(DeterministicScore::MAX)
141}
142
143#[inline]
145pub fn rrf_score(rank: usize, k: usize) -> DeterministicScore {
146 let Some(denominator) = k.checked_add(rank) else {
147 return DeterministicScore::ZERO;
148 };
149 if denominator == 0 {
150 return DeterministicScore::ZERO;
151 }
152 DeterministicScore::from_f64(1.0 / (denominator as f64))
153}
154
155#[inline]
157pub fn rrf_score_one_based(rank: NonZeroUsize, k: usize) -> DeterministicScore {
158 let Some(denominator) = k.checked_add(rank.get()) else {
159 return DeterministicScore::ZERO;
160 };
161 DeterministicScore::from_f64(1.0 / denominator as f64)
162}
163
164#[inline]
166pub fn rrf_score_zero_based(index: usize, k: usize) -> DeterministicScore {
167 let Some(rank) = index.checked_add(1).and_then(NonZeroUsize::new) else {
168 return DeterministicScore::ZERO;
169 };
170 rrf_score_one_based(rank, k)
171}
172
173const SCALE_RAW: i128 = 4_294_967_296; #[inline]
180pub fn weighted_sum(
181 scores: &[DeterministicScore],
182 weights: &[f64],
183) -> Result<DeterministicScore, ScoreError> {
184 if scores.len() != weights.len() {
185 return Err(ScoreError::LengthMismatch {
186 expected_desc: "scores and weights must have same length",
187 first_len: scores.len(),
188 second_len: weights.len(),
189 });
190 }
191 let mut acc = 0i128;
192 for (index, (&score, &weight)) in scores.iter().zip(weights.iter()).enumerate() {
193 if !weight.is_finite() {
194 return Err(ScoreError::NonFiniteWeight { index });
195 }
196 let w = DeterministicScore::from_f64(weight);
197 acc += (score.to_raw() as i128 * w.to_raw() as i128) / SCALE_RAW;
198 }
199 Ok(DeterministicScore::from_raw(acc.clamp(
200 DeterministicScore::NEG_INF.to_raw() as i128,
201 DeterministicScore::MAX.to_raw() as i128,
202 ) as i64))
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn s(v: f64) -> DeterministicScore {
210 DeterministicScore::from_f64(v)
211 }
212
213 #[test]
214 fn sum_basic() {
215 let scores = [s(0.1), s(0.2), s(0.3)];
216 let result = sum_scores(&scores);
217 assert!((result.to_f64() - 0.6).abs() < 1e-9);
218 }
219
220 #[test]
221 fn sum_empty() {
222 let result = sum_scores(&[]);
223 assert_eq!(result, DeterministicScore::ZERO);
224 }
225
226 #[test]
227 fn avg_basic() {
228 let scores = [s(0.1), s(0.2), s(0.3)];
229 let result = avg_scores(&scores);
230 assert!((result.to_f64() - 0.2).abs() < 1e-9);
231 }
232
233 #[test]
234 fn rrf_basic() {
235 let r1 = rrf_score(1, 60);
236 let r2 = rrf_score(2, 60);
237 assert!(r1 > r2);
238 assert!((r1.to_f64() - 1.0 / 61.0).abs() < 1e-9);
239 }
240
241 #[test]
242 fn weighted_sum_basic() {
243 let scores = [s(0.5), s(1.0)];
244 let weights = [0.4, 0.6];
245 let result = weighted_sum(&scores, &weights).unwrap();
246 assert!((result.to_f64() - 0.8).abs() < 1e-6);
247 }
248
249 #[test]
250 fn weighted_sum_length_mismatch() {
251 let err = weighted_sum(&[s(0.1)], &[0.5, 0.5]).unwrap_err();
252 assert!(matches!(err, ScoreError::LengthMismatch { .. }));
253 }
254
255 #[test]
256 fn weighted_sum_rejects_nan() {
257 let err = weighted_sum(&[s(0.1)], &[f64::NAN]).unwrap_err();
258 assert!(matches!(err, ScoreError::NonFiniteWeight { index: 0 }));
259 }
260
261 #[test]
262 fn sum_negative_saturation_clamps_to_neg_inf() {
263 let big_neg = DeterministicScore::NEG_INF;
264 let result = sum_scores(&[big_neg, big_neg, big_neg]);
265 assert_eq!(result, DeterministicScore::NEG_INF);
266 assert!(result.is_infinite());
267 assert_eq!(result.to_f64(), f64::NEG_INFINITY);
268 }
269
270 #[test]
271 fn avg_negative_saturation_clamps_to_neg_inf() {
272 let big_neg = DeterministicScore::NEG_INF;
273 let result = avg_scores(&[big_neg, big_neg]);
274 assert_eq!(result, DeterministicScore::NEG_INF);
275 }
276
277 #[test]
278 fn sum_order_independent() {
279 let a = DeterministicScore::from_f64(1e9);
280 let b = DeterministicScore::from_f64(-1e9);
281 let c = DeterministicScore::from_f64(0.5);
282 let r1 = sum_scores(&[a, b, c]);
283 let r2 = sum_scores(&[c, a, b]);
284 let r3 = sum_scores(&[b, c, a]);
285 assert_eq!(r1, r2);
286 assert_eq!(r2, r3);
287 }
288
289 #[test]
290 fn avg_scores_checked_empty_returns_zero_no_flag() {
291 let (mean, flag) = avg_scores_checked(&[]);
292 assert_eq!(mean, DeterministicScore::ZERO);
293 assert!(!flag);
294 }
295
296 #[test]
297 fn avg_scores_checked_near_saturation_sets_flag() {
298 let (_, flag) = avg_scores_checked(&[DeterministicScore::MAX, DeterministicScore::MAX]);
299 assert!(flag);
300 }
301
302 #[test]
303 fn max_score_empty_returns_neg_inf() {
304 assert_eq!(max_score(&[]), DeterministicScore::NEG_INF);
305 }
306
307 #[test]
308 fn min_score_empty_returns_max() {
309 assert_eq!(min_score(&[]), DeterministicScore::MAX);
310 }
311
312 #[test]
313 fn rrf_score_zero_denominator_returns_zero() {
314 assert_eq!(rrf_score(0, 0), DeterministicScore::ZERO);
315 }
316
317 #[test]
318 fn rrf_score_overflow_returns_zero() {
319 assert_eq!(rrf_score(usize::MAX, 1), DeterministicScore::ZERO);
320 }
321
322 #[test]
323 fn weighted_sum_empty_returns_zero() {
324 assert_eq!(weighted_sum(&[], &[]).unwrap(), DeterministicScore::ZERO);
325 }
326
327 #[test]
328 fn weighted_sum_rejects_infinite_weight() {
329 let err = weighted_sum(&[s(1.0)], &[f64::INFINITY]).unwrap_err();
330 assert_eq!(err, ScoreError::NonFiniteWeight { index: 0 });
331 }
332
333 #[test]
334 fn rrf_one_based_rank_1_equals_legacy_rank_1() {
335 use std::num::NonZeroUsize;
336 let one_based = rrf_score_one_based(NonZeroUsize::new(1).unwrap(), 60);
337 let legacy = rrf_score(1, 60);
338 assert_eq!(
339 one_based, legacy,
340 "rrf_score_one_based(1,60) must match rrf_score(1,60)"
341 );
342 }
343
344 #[test]
345 fn rrf_zero_based_index_0_equals_one_based_rank_1() {
346 use std::num::NonZeroUsize;
347 let zero_based = rrf_score_zero_based(0, 60);
348 let one_based = rrf_score_one_based(NonZeroUsize::new(1).unwrap(), 60);
349 assert_eq!(zero_based, one_based);
350 }
351
352 #[test]
353 fn rrf_one_based_monotone_decreasing() {
354 use std::num::NonZeroUsize;
355 let r1 = rrf_score_one_based(NonZeroUsize::new(1).unwrap(), 60);
356 let r2 = rrf_score_one_based(NonZeroUsize::new(2).unwrap(), 60);
357 let r10 = rrf_score_one_based(NonZeroUsize::new(10).unwrap(), 60);
358 assert!(r1 > r2);
359 assert!(r2 > r10);
360 }
361
362 #[test]
363 fn rrf_one_based_value_matches_formula() {
364 use std::num::NonZeroUsize;
365 let score = rrf_score_one_based(NonZeroUsize::new(1).unwrap(), 60);
366 assert!((score.to_f64() - 1.0 / 61.0).abs() < 1e-9);
367 }
368
369 #[test]
370 fn rrf_one_based_overflow_returns_zero() {
371 use std::num::NonZeroUsize;
372 let score = rrf_score_one_based(NonZeroUsize::new(usize::MAX).unwrap(), 1);
373 assert_eq!(score, DeterministicScore::ZERO);
374 }
375
376 #[test]
377 fn rrf_zero_based_overflow_returns_zero() {
378 let score = rrf_score_zero_based(usize::MAX, 1);
379 assert_eq!(score, DeterministicScore::ZERO);
380 }
381
382 #[test]
383 fn avg_scores_checked_saturation_flag_is_order_independent() {
384 let big = DeterministicScore::from_raw(i64::MAX / 2);
386 let neg = DeterministicScore::from_raw(i64::MIN / 2 + 1);
387 let scores_order_a = [big, neg, big, neg];
388 let scores_order_b = [big, big, neg, neg];
389 let (_, flag_a) = avg_scores_checked(&scores_order_a);
390 let (_, flag_b) = avg_scores_checked(&scores_order_b);
391 assert_eq!(
392 flag_a, flag_b,
393 "near_saturation flag must be order-independent: order_a={flag_a}, order_b={flag_b}"
394 );
395 }
396
397 #[test]
398 fn avg_scores_checked_normal_values_no_saturation_flag() {
399 let scores = [s(0.1), s(0.2), s(-0.1), s(0.3)];
400 let (_, flag) = avg_scores_checked(&scores);
401 assert!(!flag, "small scores should not trigger near_saturation");
402 }
403}