1use thiserror::Error;
9
10pub const MAX_CONFORMAL_SAMPLE_COUNT: u64 = u64::MAX - 1;
16
17const MAX_EXACT_METRIC_COUNT: u64 = (1_u64 << 53) - 1;
18const MAX_SHORTEST_DECIMAL_COEFFICIENT: u128 = 99_999_999_999_999_999;
19const MAX_U128_POWER_OF_TEN: u32 = 38;
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum ConformalMultiTargetPolicy {
24 Marginal,
26 JointMax,
28}
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum ConformalSmallSamplePolicy {
33 Error,
35 Unbounded,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
41pub enum ConformalRadius {
42 Finite(f64),
43 Unbounded,
44}
45
46#[derive(Clone, Debug, PartialEq)]
48pub struct SplitConformalQuantile {
49 pub coverage: f64,
50 pub rank: u64,
53 pub radii: Vec<ConformalRadius>,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
63pub enum RegressionIntervalCell {
64 Finite { lower: f64, upper: f64 },
65 Unbounded,
66}
67
68impl RegressionIntervalCell {
69 pub fn endpoints(self) -> (Option<f64>, Option<f64>) {
71 match self {
72 Self::Finite { lower, upper } => (Some(lower), Some(upper)),
73 Self::Unbounded => (None, None),
74 }
75 }
76
77 pub fn midpoint(self) -> Option<f64> {
79 match self {
80 Self::Finite { lower, upper } => Some(finite_midpoint(lower, upper)),
81 Self::Unbounded => None,
82 }
83 }
84}
85
86#[derive(Clone, Debug, PartialEq)]
88pub struct RegressionConformalInterval {
89 pub coverage: f64,
90 pub cells: Vec<Vec<RegressionIntervalCell>>,
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum ConformalMeasurementStatus {
96 Finite,
97 Unbounded,
98}
99
100#[derive(Clone, Debug, PartialEq)]
102pub struct RegressionConformalMetrics {
103 pub target_index: Option<usize>,
105 pub measurement_status: ConformalMeasurementStatus,
106 pub empirical_coverage: f64,
107 pub coverage_gap: f64,
108 pub mean_width: Option<f64>,
109 pub median_width: Option<f64>,
110 pub interval_score: Option<f64>,
111}
112
113#[derive(Clone, Debug, Eq, PartialEq, Error)]
115pub enum ConformalError {
116 #[error("conformal coverages must be non-empty")]
117 EmptyCoverages,
118
119 #[error("coverage at index {index} must be finite and strictly inside (0, 1)")]
120 InvalidCoverage { index: usize },
121
122 #[error("coverage at index {index} is not strictly greater than its predecessor")]
123 NonIncreasingCoverage { index: usize },
124
125 #[error("sample_count must be in 1..={MAX_CONFORMAL_SAMPLE_COUNT}, got {sample_count}")]
126 InvalidSampleCount { sample_count: u64 },
127
128 #[error("failed to recover a shortest decimal binary64 representation")]
129 DecimalConversion,
130
131 #[error("{matrix} matrix must be non-empty")]
132 EmptyMatrix { matrix: &'static str },
133
134 #[error("{matrix} row {row} must be non-empty")]
135 EmptyMatrixRow { matrix: &'static str, row: usize },
136
137 #[error("{matrix} row {row} has width {actual}, expected the rectangular width {expected}")]
138 RaggedMatrix {
139 matrix: &'static str,
140 row: usize,
141 expected: usize,
142 actual: usize,
143 },
144
145 #[error("{matrix} value at row {row}, target {target} must be finite")]
146 NonFiniteMatrixValue {
147 matrix: &'static str,
148 row: usize,
149 target: usize,
150 },
151
152 #[error("residual at row {row}, target {target} must be non-negative")]
153 NegativeResidual { row: usize, target: usize },
154
155 #[error("finite-sample rank {rank} exceeds calibration size {sample_count}")]
156 SmallSampleRank { rank: u64, sample_count: u64 },
157
158 #[error("split-conformal quantiles must be non-empty")]
159 EmptyQuantiles,
160
161 #[error("quantile {coverage_index} has rank zero")]
162 ZeroQuantileRank { coverage_index: usize },
163
164 #[error("quantile rank decreases at coverage index {coverage_index}")]
165 DecreasingQuantileRank { coverage_index: usize },
166
167 #[error(
168 "quantile {coverage_index} has {actual} radii, expected {expected} for this target policy"
169 )]
170 QuantileShape {
171 coverage_index: usize,
172 expected: usize,
173 actual: usize,
174 },
175
176 #[error("quantile {coverage_index}, radius {radius_index} must be finite and non-negative")]
177 InvalidRadius {
178 coverage_index: usize,
179 radius_index: usize,
180 },
181
182 #[error("quantile {coverage_index} mixes finite and unbounded radii")]
183 MixedRadiusStatus { coverage_index: usize },
184
185 #[error(
186 "quantile radius is not nested at coverage index {coverage_index}, radius {radius_index}"
187 )]
188 NonNestedRadius {
189 coverage_index: usize,
190 radius_index: usize,
191 },
192
193 #[error("{left} and {right} matrix row counts differ")]
194 MatrixRowCountMismatch {
195 left: &'static str,
196 right: &'static str,
197 },
198
199 #[error("{left} and {right} matrix target widths differ")]
200 MatrixTargetCountMismatch {
201 left: &'static str,
202 right: &'static str,
203 },
204
205 #[error("interval cell at row {row}, target {target} must contain ordered finite bounds")]
206 InvalidIntervalCell { row: usize, target: usize },
207
208 #[error("finite arithmetic overflow while computing {operation}")]
209 ArithmeticOverflow { operation: &'static str },
210
211 #[error(
212 "finite interval at coverage {coverage_index}, row {row}, target {target} cannot preserve the W0 decimal midpoint and radius closures"
213 )]
214 UnrepresentableInterval {
215 coverage_index: usize,
216 row: usize,
217 target: usize,
218 },
219
220 #[error("metric cell count exceeds the exact binary64 integer range")]
221 MetricCountTooLarge,
222}
223
224pub fn validate_conformal_coverages(coverages: &[f64]) -> Result<(), ConformalError> {
226 if coverages.is_empty() {
227 return Err(ConformalError::EmptyCoverages);
228 }
229 for (index, coverage) in coverages.iter().copied().enumerate() {
230 if !(coverage.is_finite() && 0.0 < coverage && coverage < 1.0) {
231 return Err(ConformalError::InvalidCoverage { index });
232 }
233 if index > 0 && coverages[index - 1] >= coverage {
234 return Err(ConformalError::NonIncreasingCoverage { index });
235 }
236 }
237 Ok(())
238}
239
240pub fn finite_sample_conformal_rank(
248 sample_count: u64,
249 coverage: f64,
250) -> Result<u64, ConformalError> {
251 if sample_count == 0 || sample_count > MAX_CONFORMAL_SAMPLE_COUNT {
252 return Err(ConformalError::InvalidSampleCount { sample_count });
253 }
254 validate_conformal_coverages(&[coverage])?;
255 let decimal = shortest_decimal(coverage)?;
256 let multiplier = u128::from(
257 sample_count
258 .checked_add(1)
259 .ok_or(ConformalError::InvalidSampleCount { sample_count })?,
260 );
261 let scaled =
262 multiplier
263 .checked_mul(decimal.coefficient)
264 .ok_or(ConformalError::ArithmeticOverflow {
265 operation: "finite-sample rank numerator",
266 })?;
267
268 let rank = if decimal.scale > MAX_U128_POWER_OF_TEN {
273 1_u128
274 } else {
275 let denominator = checked_power_of_ten(decimal.scale)?;
276 let quotient = scaled / denominator;
277 quotient + u128::from(scaled % denominator != 0)
278 };
279 u64::try_from(rank).map_err(|_| ConformalError::ArithmeticOverflow {
280 operation: "finite-sample rank result",
281 })
282}
283
284pub fn split_absolute_residual_quantiles(
289 residuals: &[Vec<f64>],
290 coverages: &[f64],
291 multi_target_policy: ConformalMultiTargetPolicy,
292 small_sample_policy: ConformalSmallSamplePolicy,
293) -> Result<Vec<SplitConformalQuantile>, ConformalError> {
294 let target_count = validate_finite_matrix(residuals, "residuals", true)?;
295 validate_conformal_coverages(coverages)?;
296 let sample_count =
297 u64::try_from(residuals.len()).map_err(|_| ConformalError::InvalidSampleCount {
298 sample_count: u64::MAX,
299 })?;
300 if sample_count > MAX_CONFORMAL_SAMPLE_COUNT {
301 return Err(ConformalError::InvalidSampleCount { sample_count });
302 }
303
304 let score_count = match multi_target_policy {
305 ConformalMultiTargetPolicy::Marginal => target_count,
306 ConformalMultiTargetPolicy::JointMax => 1,
307 };
308 let mut ordered_scores = (0..score_count)
309 .map(|_| Vec::with_capacity(residuals.len()))
310 .collect::<Vec<_>>();
311 for row in residuals {
312 match multi_target_policy {
313 ConformalMultiTargetPolicy::Marginal => {
314 for (target, residual) in row.iter().copied().enumerate() {
315 ordered_scores[target].push(normalized_zero(residual));
316 }
317 }
318 ConformalMultiTargetPolicy::JointMax => {
319 let maximum = row
320 .iter()
321 .copied()
322 .map(normalized_zero)
323 .fold(0.0_f64, f64::max);
324 ordered_scores[0].push(maximum);
325 }
326 }
327 }
328 for scores in &mut ordered_scores {
329 scores.sort_by(f64::total_cmp);
330 }
331
332 let mut quantiles = Vec::with_capacity(coverages.len());
333 for coverage in coverages.iter().copied() {
334 let rank = finite_sample_conformal_rank(sample_count, coverage)?;
335 let radii = if rank > sample_count {
336 match small_sample_policy {
337 ConformalSmallSamplePolicy::Error => {
338 return Err(ConformalError::SmallSampleRank { rank, sample_count });
339 }
340 ConformalSmallSamplePolicy::Unbounded => {
341 vec![ConformalRadius::Unbounded; score_count]
342 }
343 }
344 } else {
345 let index =
346 usize::try_from(rank - 1).map_err(|_| ConformalError::ArithmeticOverflow {
347 operation: "quantile order-statistic index",
348 })?;
349 ordered_scores
350 .iter()
351 .map(|scores| ConformalRadius::Finite(scores[index]))
352 .collect()
353 };
354 quantiles.push(SplitConformalQuantile {
355 coverage,
356 rank,
357 radii,
358 });
359 }
360 Ok(quantiles)
361}
362
363pub fn apply_split_absolute_residual(
373 point_predictions: &[Vec<f64>],
374 quantiles: &[SplitConformalQuantile],
375 multi_target_policy: ConformalMultiTargetPolicy,
376) -> Result<Vec<RegressionConformalInterval>, ConformalError> {
377 let target_count = validate_finite_matrix(point_predictions, "point predictions", false)?;
378 validate_quantiles(quantiles, target_count, multi_target_policy)?;
379
380 let mut intervals = Vec::with_capacity(quantiles.len());
381 for (coverage_index, quantile) in quantiles.iter().enumerate() {
382 let mut cells = Vec::with_capacity(point_predictions.len());
383 for (row_index, row) in point_predictions.iter().enumerate() {
384 let mut interval_row = Vec::with_capacity(target_count);
385 for (target, point) in row.iter().copied().enumerate() {
386 let radius = quantile.radii[match multi_target_policy {
387 ConformalMultiTargetPolicy::Marginal => target,
388 ConformalMultiTargetPolicy::JointMax => 0,
389 }];
390 let cell = match radius {
391 ConformalRadius::Unbounded => RegressionIntervalCell::Unbounded,
392 ConformalRadius::Finite(radius) => {
393 let radius = normalized_zero(radius);
394 let (lower, upper) = decimal_conformal_endpoints(point, radius)?;
395 if !decimal_interval_closes(point, radius, lower, upper)? {
396 return Err(ConformalError::UnrepresentableInterval {
397 coverage_index,
398 row: row_index,
399 target,
400 });
401 }
402 RegressionIntervalCell::Finite { lower, upper }
403 }
404 };
405 interval_row.push(cell);
406 }
407 cells.push(interval_row);
408 }
409 intervals.push(RegressionConformalInterval {
410 coverage: quantile.coverage,
411 cells,
412 });
413 }
414 Ok(intervals)
415}
416
417pub fn regression_conformal_metrics(
425 truth: &[Vec<f64>],
426 interval: &RegressionConformalInterval,
427 multi_target_policy: ConformalMultiTargetPolicy,
428) -> Result<Vec<RegressionConformalMetrics>, ConformalError> {
429 validate_conformal_coverages(&[interval.coverage])?;
430 let target_count = validate_finite_matrix(truth, "truth", false)?;
431 let interval_target_count = validate_interval_matrix(&interval.cells)?;
432 if truth.len() != interval.cells.len() {
433 return Err(ConformalError::MatrixRowCountMismatch {
434 left: "truth",
435 right: "interval",
436 });
437 }
438 if target_count != interval_target_count {
439 return Err(ConformalError::MatrixTargetCountMismatch {
440 left: "truth",
441 right: "interval",
442 });
443 }
444
445 let alpha = 1.0 - interval.coverage;
446 let miss_scale = 2.0 / alpha;
447 let mut covered = Vec::with_capacity(truth.len());
448 let mut widths = Vec::with_capacity(truth.len());
449 let mut scores = Vec::with_capacity(truth.len());
450 for (row_index, (truth_row, interval_row)) in truth.iter().zip(&interval.cells).enumerate() {
451 let mut covered_row = Vec::with_capacity(target_count);
452 let mut width_row = Vec::with_capacity(target_count);
453 let mut score_row = Vec::with_capacity(target_count);
454 for (target, (value, cell)) in truth_row
455 .iter()
456 .copied()
457 .zip(interval_row.iter().copied())
458 .enumerate()
459 {
460 match cell {
461 RegressionIntervalCell::Unbounded => {
462 covered_row.push(true);
463 width_row.push(None);
464 score_row.push(None);
465 }
466 RegressionIntervalCell::Finite { lower, upper } => {
467 if !lower.is_finite() || !upper.is_finite() || lower > upper {
468 return Err(ConformalError::InvalidIntervalCell {
469 row: row_index,
470 target,
471 });
472 }
473 let width = checked_finite(upper - lower, "interval width")?;
474 let miss_distance = if value < lower {
475 lower - value
476 } else if value > upper {
477 value - upper
478 } else {
479 0.0
480 };
481 let penalty = checked_finite(
482 miss_scale * checked_finite(miss_distance, "interval miss distance")?,
483 "Winkler miss penalty",
484 )?;
485 let score = checked_finite(width + penalty, "Winkler interval score")?;
486 covered_row.push(lower <= value && value <= upper);
487 width_row.push(Some(width));
488 score_row.push(Some(score));
489 }
490 }
491 }
492 covered.push(covered_row);
493 widths.push(width_row);
494 scores.push(score_row);
495 }
496
497 match multi_target_policy {
498 ConformalMultiTargetPolicy::Marginal => (0..target_count)
499 .map(|target| {
500 summarize_metrics(
501 interval.coverage,
502 covered.iter().map(|row| row[target]).collect(),
503 widths.iter().map(|row| row[target]).collect(),
504 scores.iter().map(|row| row[target]).collect(),
505 Some(target),
506 )
507 })
508 .collect(),
509 ConformalMultiTargetPolicy::JointMax => summarize_metrics(
510 interval.coverage,
511 covered
512 .iter()
513 .map(|row| row.iter().all(|value| *value))
514 .collect(),
515 widths.iter().flatten().copied().collect(),
516 scores.iter().flatten().copied().collect(),
517 None,
518 )
519 .map(|summary| vec![summary]),
520 }
521}
522
523#[derive(Clone, Copy, Debug)]
524struct ShortestDecimal {
525 coefficient: u128,
526 scale: u32,
527}
528
529#[derive(Clone, Debug, Eq, PartialEq)]
534struct ExactDecimal {
535 negative: bool,
536 digits: Vec<u8>,
537 exponent: i32,
538}
539
540impl ExactDecimal {
541 fn zero(negative: bool) -> Self {
542 Self {
543 negative,
544 digits: vec![0],
545 exponent: 0,
546 }
547 }
548
549 fn is_zero(&self) -> bool {
550 self.digits == [0]
551 }
552}
553
554fn shortest_decimal(value: f64) -> Result<ShortestDecimal, ConformalError> {
555 let rendered = value.to_string();
556 let (mantissa, exponent) = match rendered.find(['e', 'E']) {
557 Some(index) => {
558 let exponent = rendered[index + 1..]
559 .parse::<i32>()
560 .map_err(|_| ConformalError::DecimalConversion)?;
561 (&rendered[..index], exponent)
562 }
563 None => (rendered.as_str(), 0),
564 };
565 let mut coefficient = 0_u128;
566 let mut fractional_digits = 0_i32;
567 let mut after_decimal = false;
568 let mut saw_digit = false;
569 for byte in mantissa.bytes() {
570 match byte {
571 b'.' if !after_decimal => after_decimal = true,
572 b'0'..=b'9' => {
573 saw_digit = true;
574 coefficient = coefficient
575 .checked_mul(10)
576 .and_then(|current| current.checked_add(u128::from(byte - b'0')))
577 .ok_or(ConformalError::DecimalConversion)?;
578 if after_decimal {
579 fractional_digits = fractional_digits
580 .checked_add(1)
581 .ok_or(ConformalError::DecimalConversion)?;
582 }
583 }
584 _ => return Err(ConformalError::DecimalConversion),
585 }
586 }
587 if !saw_digit || coefficient == 0 || coefficient > MAX_SHORTEST_DECIMAL_COEFFICIENT {
588 return Err(ConformalError::DecimalConversion);
589 }
590 let scale = fractional_digits
591 .checked_sub(exponent)
592 .ok_or(ConformalError::DecimalConversion)?;
593 if scale <= 0 {
594 return Err(ConformalError::DecimalConversion);
595 }
596 Ok(ShortestDecimal {
597 coefficient,
598 scale: u32::try_from(scale).map_err(|_| ConformalError::DecimalConversion)?,
599 })
600}
601
602fn exact_decimal_from_f64(value: f64) -> Result<ExactDecimal, ConformalError> {
603 if !value.is_finite() {
604 return Err(ConformalError::DecimalConversion);
605 }
606 let rendered = value.to_string();
607 let (negative, unsigned) = rendered
608 .strip_prefix('-')
609 .map_or((false, rendered.as_str()), |rest| (true, rest));
610 let (mantissa, scientific_exponent) = match unsigned.find(['e', 'E']) {
611 Some(index) => {
612 let exponent = unsigned[index + 1..]
613 .parse::<i32>()
614 .map_err(|_| ConformalError::DecimalConversion)?;
615 (&unsigned[..index], exponent)
616 }
617 None => (unsigned, 0),
618 };
619
620 let mut digits = Vec::with_capacity(mantissa.len());
621 let mut fractional_digits = 0_i32;
622 let mut after_decimal = false;
623 for byte in mantissa.bytes() {
624 match byte {
625 b'.' if !after_decimal => after_decimal = true,
626 b'0'..=b'9' => {
627 digits.push(byte - b'0');
628 if after_decimal {
629 fractional_digits = fractional_digits
630 .checked_add(1)
631 .ok_or(ConformalError::DecimalConversion)?;
632 }
633 }
634 _ => return Err(ConformalError::DecimalConversion),
635 }
636 }
637 if digits.is_empty() {
638 return Err(ConformalError::DecimalConversion);
639 }
640
641 let first_nonzero = digits.iter().position(|digit| *digit != 0);
642 let Some(first_nonzero) = first_nonzero else {
643 return Ok(ExactDecimal::zero(negative));
644 };
645 digits.drain(..first_nonzero);
646 let exponent = scientific_exponent
647 .checked_sub(fractional_digits)
648 .ok_or(ConformalError::DecimalConversion)?;
649 normalize_exact_decimal(ExactDecimal {
650 negative,
651 digits,
652 exponent,
653 })
654}
655
656fn normalize_exact_decimal(mut value: ExactDecimal) -> Result<ExactDecimal, ConformalError> {
657 while value.digits.len() > 1 && value.digits.last() == Some(&0) {
658 value.digits.pop();
659 value.exponent = value
660 .exponent
661 .checked_add(1)
662 .ok_or(ConformalError::DecimalConversion)?;
663 }
664 Ok(value)
665}
666
667fn aligned_decimal_digits(
668 value: &ExactDecimal,
669 common_exponent: i32,
670) -> Result<Vec<u8>, ConformalError> {
671 let trailing_zeros = value
672 .exponent
673 .checked_sub(common_exponent)
674 .and_then(|count| usize::try_from(count).ok())
675 .ok_or(ConformalError::DecimalConversion)?;
676 let expanded_len = value
677 .digits
678 .len()
679 .checked_add(trailing_zeros)
680 .ok_or(ConformalError::DecimalConversion)?;
681 let mut digits = Vec::with_capacity(expanded_len);
682 digits.extend_from_slice(&value.digits);
683 digits.resize(expanded_len, 0);
684 Ok(digits)
685}
686
687fn add_decimal_magnitudes(left: &[u8], right: &[u8]) -> Vec<u8> {
688 let digit_count = left.len().max(right.len());
689 let mut left = left.iter().rev();
690 let mut right = right.iter().rev();
691 let mut reversed = Vec::with_capacity(digit_count + 1);
692 let mut carry = 0_u8;
693 for _ in 0..digit_count {
694 let sum = left.next().copied().unwrap_or(0) + right.next().copied().unwrap_or(0) + carry;
695 reversed.push(sum % 10);
696 carry = sum / 10;
697 }
698 if carry != 0 {
699 reversed.push(carry);
700 }
701 reversed.reverse();
702 reversed
703}
704
705fn subtract_decimal_magnitudes(larger: &[u8], smaller: &[u8]) -> Result<Vec<u8>, ConformalError> {
706 let mut smaller = smaller.iter().rev();
707 let mut reversed = Vec::with_capacity(larger.len());
708 let mut borrow = 0_i16;
709 for larger_digit in larger.iter().rev().copied() {
710 let smaller_digit = i16::from(smaller.next().copied().unwrap_or(0));
711 let mut difference = i16::from(larger_digit) - smaller_digit - borrow;
712 if difference < 0 {
713 difference += 10;
714 borrow = 1;
715 } else {
716 borrow = 0;
717 }
718 reversed.push(u8::try_from(difference).map_err(|_| ConformalError::DecimalConversion)?);
719 }
720 if borrow != 0 {
721 return Err(ConformalError::DecimalConversion);
722 }
723 reversed.reverse();
724 let leading_zeros = reversed
725 .iter()
726 .position(|digit| *digit != 0)
727 .unwrap_or(reversed.len().saturating_sub(1));
728 reversed.drain(..leading_zeros);
729 Ok(reversed)
730}
731
732fn add_exact_decimals(
733 left: &ExactDecimal,
734 right: &ExactDecimal,
735 subtract_right: bool,
736) -> Result<ExactDecimal, ConformalError> {
737 if left.is_zero() && right.is_zero() {
738 return Ok(ExactDecimal::zero(false));
739 }
740 if right.is_zero() {
741 return Ok(left.clone());
742 }
743 if left.is_zero() {
744 let mut result = right.clone();
745 result.negative ^= subtract_right;
746 return Ok(result);
747 }
748
749 let common_exponent = left.exponent.min(right.exponent);
750 let left_digits = aligned_decimal_digits(left, common_exponent)?;
751 let right_digits = aligned_decimal_digits(right, common_exponent)?;
752 let right_negative = right.negative ^ subtract_right;
753 let (negative, digits) = if left.negative == right_negative {
754 (
755 left.negative,
756 add_decimal_magnitudes(&left_digits, &right_digits),
757 )
758 } else {
759 let magnitude_order = left_digits
760 .len()
761 .cmp(&right_digits.len())
762 .then_with(|| left_digits.cmp(&right_digits));
763 match magnitude_order {
764 std::cmp::Ordering::Greater => (
765 left.negative,
766 subtract_decimal_magnitudes(&left_digits, &right_digits)?,
767 ),
768 std::cmp::Ordering::Less => (
769 right_negative,
770 subtract_decimal_magnitudes(&right_digits, &left_digits)?,
771 ),
772 std::cmp::Ordering::Equal => return Ok(ExactDecimal::zero(false)),
773 }
774 };
775 normalize_exact_decimal(ExactDecimal {
776 negative,
777 digits,
778 exponent: common_exponent,
779 })
780}
781
782fn exact_decimal_to_f64(
783 value: &ExactDecimal,
784 operation: &'static str,
785) -> Result<f64, ConformalError> {
786 if value.is_zero() {
787 return Ok(f64::from_bits(u64::from(value.negative) << 63));
788 }
789 let scientific_exponent = value
790 .exponent
791 .checked_add(
792 i32::try_from(value.digits.len() - 1).map_err(|_| ConformalError::DecimalConversion)?,
793 )
794 .ok_or(ConformalError::DecimalConversion)?;
795 let mut rendered = String::with_capacity(value.digits.len() + 16);
796 if value.negative {
797 rendered.push('-');
798 }
799 rendered.push(char::from(b'0' + value.digits[0]));
800 if value.digits.len() > 1 {
801 rendered.push('.');
802 rendered.extend(
803 value.digits[1..]
804 .iter()
805 .map(|digit| char::from(b'0' + digit)),
806 );
807 }
808 rendered.push('e');
809 rendered.push_str(&scientific_exponent.to_string());
810 let rounded = rendered
811 .parse::<f64>()
812 .map_err(|_| ConformalError::DecimalConversion)?;
813 checked_finite(rounded, operation)
814}
815
816fn decimal_conformal_endpoints(point: f64, radius: f64) -> Result<(f64, f64), ConformalError> {
817 let point_decimal = exact_decimal_from_f64(point)?;
818 let radius_decimal = exact_decimal_from_f64(radius)?;
819 let mut lower_decimal = add_exact_decimals(&point_decimal, &radius_decimal, true)?;
820 let mut upper_decimal = add_exact_decimals(&point_decimal, &radius_decimal, false)?;
821
822 if lower_decimal.is_zero() {
825 lower_decimal.negative = point == 0.0 && point.is_sign_negative();
826 }
827 if upper_decimal.is_zero() {
828 upper_decimal.negative = false;
829 }
830
831 Ok((
832 exact_decimal_to_f64(&lower_decimal, "finite conformal lower endpoint")?,
833 exact_decimal_to_f64(&upper_decimal, "finite conformal upper endpoint")?,
834 ))
835}
836
837fn exact_decimal_values_equal(left: &ExactDecimal, right: &ExactDecimal) -> bool {
838 (left.is_zero() && right.is_zero()) || left == right
839}
840
841fn decimal_interval_closes(
842 point: f64,
843 radius: f64,
844 lower: f64,
845 upper: f64,
846) -> Result<bool, ConformalError> {
847 let point = exact_decimal_from_f64(point)?;
848 let radius = exact_decimal_from_f64(radius)?;
849 let lower = exact_decimal_from_f64(lower)?;
850 let upper = exact_decimal_from_f64(upper)?;
851 let endpoint_midpoint = add_exact_decimals(&lower, &upper, false)?;
852 let expected_midpoint = add_exact_decimals(&point, &point, false)?;
853 let endpoint_width = add_exact_decimals(&upper, &lower, true)?;
854 let expected_width = add_exact_decimals(&radius, &radius, false)?;
855 Ok(
856 exact_decimal_values_equal(&endpoint_midpoint, &expected_midpoint)
857 && exact_decimal_values_equal(&endpoint_width, &expected_width),
858 )
859}
860
861fn checked_power_of_ten(power: u32) -> Result<u128, ConformalError> {
862 let mut value = 1_u128;
863 for _ in 0..power {
864 value = value
865 .checked_mul(10)
866 .ok_or(ConformalError::ArithmeticOverflow {
867 operation: "decimal rank denominator",
868 })?;
869 }
870 Ok(value)
871}
872
873fn validate_finite_matrix(
874 matrix: &[Vec<f64>],
875 name: &'static str,
876 non_negative: bool,
877) -> Result<usize, ConformalError> {
878 if matrix.is_empty() {
879 return Err(ConformalError::EmptyMatrix { matrix: name });
880 }
881 let expected = matrix[0].len();
882 if expected == 0 {
883 return Err(ConformalError::EmptyMatrixRow {
884 matrix: name,
885 row: 0,
886 });
887 }
888 for (row_index, row) in matrix.iter().enumerate() {
889 if row.len() != expected {
890 return Err(ConformalError::RaggedMatrix {
891 matrix: name,
892 row: row_index,
893 expected,
894 actual: row.len(),
895 });
896 }
897 for (target, value) in row.iter().copied().enumerate() {
898 if !value.is_finite() {
899 return Err(ConformalError::NonFiniteMatrixValue {
900 matrix: name,
901 row: row_index,
902 target,
903 });
904 }
905 if non_negative && value < 0.0 {
906 return Err(ConformalError::NegativeResidual {
907 row: row_index,
908 target,
909 });
910 }
911 }
912 }
913 Ok(expected)
914}
915
916fn validate_interval_matrix(
917 matrix: &[Vec<RegressionIntervalCell>],
918) -> Result<usize, ConformalError> {
919 if matrix.is_empty() {
920 return Err(ConformalError::EmptyMatrix { matrix: "interval" });
921 }
922 let expected = matrix[0].len();
923 if expected == 0 {
924 return Err(ConformalError::EmptyMatrixRow {
925 matrix: "interval",
926 row: 0,
927 });
928 }
929 for (row, cells) in matrix.iter().enumerate() {
930 if cells.len() != expected {
931 return Err(ConformalError::RaggedMatrix {
932 matrix: "interval",
933 row,
934 expected,
935 actual: cells.len(),
936 });
937 }
938 for (target, cell) in cells.iter().copied().enumerate() {
939 if let RegressionIntervalCell::Finite { lower, upper } = cell {
940 if !lower.is_finite() || !upper.is_finite() || lower > upper {
941 return Err(ConformalError::InvalidIntervalCell { row, target });
942 }
943 }
944 }
945 }
946 Ok(expected)
947}
948
949fn validate_quantiles(
950 quantiles: &[SplitConformalQuantile],
951 target_count: usize,
952 policy: ConformalMultiTargetPolicy,
953) -> Result<(), ConformalError> {
954 if quantiles.is_empty() {
955 return Err(ConformalError::EmptyQuantiles);
956 }
957 let coverages = quantiles
958 .iter()
959 .map(|quantile| quantile.coverage)
960 .collect::<Vec<_>>();
961 validate_conformal_coverages(&coverages)?;
962 let expected = match policy {
963 ConformalMultiTargetPolicy::Marginal => target_count,
964 ConformalMultiTargetPolicy::JointMax => 1,
965 };
966 let mut previous_rank = 0_u64;
967 let mut previous = vec![None; expected];
968 for (coverage_index, quantile) in quantiles.iter().enumerate() {
969 if quantile.rank == 0 {
970 return Err(ConformalError::ZeroQuantileRank { coverage_index });
971 }
972 if coverage_index > 0 && quantile.rank < previous_rank {
973 return Err(ConformalError::DecreasingQuantileRank { coverage_index });
974 }
975 previous_rank = quantile.rank;
976 if quantile.radii.len() != expected {
977 return Err(ConformalError::QuantileShape {
978 coverage_index,
979 expected,
980 actual: quantile.radii.len(),
981 });
982 }
983 let first_is_unbounded = matches!(quantile.radii[0], ConformalRadius::Unbounded);
984 if quantile
985 .radii
986 .iter()
987 .any(|radius| matches!(radius, ConformalRadius::Unbounded) != first_is_unbounded)
988 {
989 return Err(ConformalError::MixedRadiusStatus { coverage_index });
990 }
991 for (radius_index, radius) in quantile.radii.iter().copied().enumerate() {
992 if let ConformalRadius::Finite(value) = radius {
993 if !value.is_finite() || value < 0.0 {
994 return Err(ConformalError::InvalidRadius {
995 coverage_index,
996 radius_index,
997 });
998 }
999 }
1000 if let Some(previous_radius) = previous[radius_index] {
1001 let nested = match (previous_radius, radius) {
1002 (ConformalRadius::Finite(left), ConformalRadius::Finite(right)) => {
1003 left <= right
1004 }
1005 (ConformalRadius::Finite(_), ConformalRadius::Unbounded)
1006 | (ConformalRadius::Unbounded, ConformalRadius::Unbounded) => true,
1007 (ConformalRadius::Unbounded, ConformalRadius::Finite(_)) => false,
1008 };
1009 if !nested {
1010 return Err(ConformalError::NonNestedRadius {
1011 coverage_index,
1012 radius_index,
1013 });
1014 }
1015 }
1016 previous[radius_index] = Some(radius);
1017 }
1018 }
1019 Ok(())
1020}
1021
1022fn summarize_metrics(
1023 coverage: f64,
1024 covered: Vec<bool>,
1025 widths: Vec<Option<f64>>,
1026 scores: Vec<Option<f64>>,
1027 target_index: Option<usize>,
1028) -> Result<RegressionConformalMetrics, ConformalError> {
1029 let count = u64::try_from(covered.len()).map_err(|_| ConformalError::MetricCountTooLarge)?;
1030 if count == 0 || count > MAX_EXACT_METRIC_COUNT {
1031 return Err(ConformalError::MetricCountTooLarge);
1032 }
1033 let measurement_count =
1034 u64::try_from(widths.len()).map_err(|_| ConformalError::MetricCountTooLarge)?;
1035 if measurement_count == 0
1036 || measurement_count > MAX_EXACT_METRIC_COUNT
1037 || widths.len() != scores.len()
1038 {
1039 return Err(ConformalError::MetricCountTooLarge);
1040 }
1041 let covered_count = u64::try_from(covered.iter().filter(|value| **value).count())
1042 .map_err(|_| ConformalError::MetricCountTooLarge)?;
1043 let empirical_coverage = (covered_count as f64) / (count as f64);
1044 let coverage_gap = empirical_coverage - coverage;
1045 if widths.iter().chain(&scores).any(Option::is_none) {
1046 return Ok(RegressionConformalMetrics {
1047 target_index,
1048 measurement_status: ConformalMeasurementStatus::Unbounded,
1049 empirical_coverage,
1050 coverage_gap,
1051 mean_width: None,
1052 median_width: None,
1053 interval_score: None,
1054 });
1055 }
1056
1057 let mut finite_widths = widths.into_iter().flatten().collect::<Vec<_>>();
1058 let finite_scores = scores.into_iter().flatten().collect::<Vec<_>>();
1059 finite_widths.sort_by(f64::total_cmp);
1060 let mean_width = checked_mean(&finite_widths, "mean interval width")?;
1061 let interval_score = checked_mean(&finite_scores, "mean Winkler interval score")?;
1062 let middle = finite_widths.len() / 2;
1063 let median_width = if finite_widths.len() % 2 == 1 {
1064 finite_widths[middle]
1065 } else {
1066 finite_midpoint(finite_widths[middle - 1], finite_widths[middle])
1067 };
1068 Ok(RegressionConformalMetrics {
1069 target_index,
1070 measurement_status: ConformalMeasurementStatus::Finite,
1071 empirical_coverage,
1072 coverage_gap,
1073 mean_width: Some(mean_width),
1074 median_width: Some(median_width),
1075 interval_score: Some(interval_score),
1076 })
1077}
1078
1079fn checked_mean(values: &[f64], operation: &'static str) -> Result<f64, ConformalError> {
1080 if values.is_empty() {
1081 return Err(ConformalError::MetricCountTooLarge);
1082 }
1083 let count = u64::try_from(values.len()).map_err(|_| ConformalError::MetricCountTooLarge)?;
1084 let mut sum = 0.0_f64;
1085 let mut sum_is_finite = true;
1086 for value in values.iter().copied() {
1087 let next = sum + value;
1088 if !next.is_finite() {
1089 sum_is_finite = false;
1090 break;
1091 }
1092 sum = next;
1093 }
1094 if sum_is_finite {
1095 return Ok(sum / (count as f64));
1099 }
1100
1101 let mut mean = 0.0_f64;
1102 for (index, value) in values.iter().copied().enumerate() {
1103 let count = u64::try_from(index + 1).map_err(|_| ConformalError::MetricCountTooLarge)?;
1104 let delta = checked_finite(value - mean, operation)?;
1105 mean = checked_finite(mean + (delta / (count as f64)), operation)?;
1106 }
1107 Ok(mean)
1108}
1109
1110fn checked_finite(value: f64, operation: &'static str) -> Result<f64, ConformalError> {
1111 if value.is_finite() {
1112 Ok(value)
1113 } else {
1114 Err(ConformalError::ArithmeticOverflow { operation })
1115 }
1116}
1117
1118fn normalized_zero(value: f64) -> f64 {
1119 if value == 0.0 {
1120 0.0
1121 } else {
1122 value
1123 }
1124}
1125
1126fn finite_midpoint(lower: f64, upper: f64) -> f64 {
1127 let opposite_signs = lower.is_sign_negative() != upper.is_sign_negative();
1128 if opposite_signs || (lower.abs() <= f64::MAX / 2.0 && upper.abs() <= f64::MAX / 2.0) {
1129 (lower + upper) / 2.0
1130 } else {
1131 (lower / 2.0) + (upper / 2.0)
1132 }
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138
1139 fn assert_close(actual: f64, expected: f64) {
1140 assert!(
1141 (actual - expected).abs() <= 1e-12,
1142 "expected {expected:?}, got {actual:?}"
1143 );
1144 }
1145
1146 fn finite(value: f64) -> ConformalRadius {
1147 ConformalRadius::Finite(value)
1148 }
1149
1150 fn finite_cell(lower: f64, upper: f64) -> RegressionIntervalCell {
1151 RegressionIntervalCell::Finite { lower, upper }
1152 }
1153
1154 #[test]
1155 fn exact_rank_matches_frozen_standard_coverages() {
1156 let cases = [(0.8, 17), (0.9, 19), (0.95, 20), (0.99, 21), (0.999, 21)];
1157 for (coverage, rank) in cases {
1158 assert_eq!(finite_sample_conformal_rank(20, coverage).unwrap(), rank);
1159 }
1160 }
1161
1162 #[test]
1163 fn exact_rank_avoids_naive_binary64_boundary_drift() {
1164 assert_eq!((25.0_f64 * 0.28).ceil() as u64, 8);
1165 assert_eq!(finite_sample_conformal_rank(24, 0.28).unwrap(), 7);
1166
1167 let above_one_third = 0.333_333_333_333_333_37_f64;
1168 assert_eq!((3.0 * above_one_third).ceil() as u64, 1);
1169 assert_eq!(finite_sample_conformal_rank(2, above_one_third).unwrap(), 2);
1170 }
1171
1172 #[test]
1173 fn exact_rank_handles_binary64_extremes_and_sample_limit() {
1174 let minimum_subnormal = f64::from_bits(1);
1175 assert_eq!(
1176 finite_sample_conformal_rank(MAX_CONFORMAL_SAMPLE_COUNT, minimum_subnormal).unwrap(),
1177 1
1178 );
1179 assert_eq!(
1180 finite_sample_conformal_rank(MAX_CONFORMAL_SAMPLE_COUNT, f64::MIN_POSITIVE).unwrap(),
1181 1
1182 );
1183 assert_eq!(
1184 finite_sample_conformal_rank(1, f64::from_bits(1.0_f64.to_bits() - 1)).unwrap(),
1185 2
1186 );
1187 assert!(matches!(
1188 finite_sample_conformal_rank(u64::MAX, 0.5),
1189 Err(ConformalError::InvalidSampleCount { .. })
1190 ));
1191 assert_eq!(
1192 finite_sample_conformal_rank(MAX_CONFORMAL_SAMPLE_COUNT, 0.999_999_999_999_999_9)
1193 .unwrap(),
1194 18_446_744_073_709_549_771
1195 );
1196 }
1197
1198 #[test]
1199 fn exact_rank_exercises_the_u128_power_of_ten_boundary() {
1200 assert_eq!(checked_power_of_ten(38).unwrap(), 10_u128.pow(38));
1201 assert_eq!(finite_sample_conformal_rank(1, 1.0e-38).unwrap(), 1);
1202 }
1203
1204 #[test]
1205 fn calibration_covers_odd_even_ties_and_small_n() {
1206 let odd = vec![vec![1.0], vec![2.0], vec![3.0], vec![4.0], vec![5.0]];
1207 let even = vec![vec![1.0], vec![2.0], vec![3.0], vec![4.0]];
1208 let ties = vec![vec![1.0], vec![2.0], vec![2.0], vec![4.0]];
1209 for (residuals, expected) in [(&odd, 3.0), (&even, 3.0), (&ties, 2.0)] {
1210 let result = split_absolute_residual_quantiles(
1211 residuals,
1212 &[0.5],
1213 ConformalMultiTargetPolicy::Marginal,
1214 ConformalSmallSamplePolicy::Error,
1215 )
1216 .unwrap();
1217 assert_eq!(result[0].rank, 3);
1218 assert_eq!(result[0].radii, vec![finite(expected)]);
1219 }
1220
1221 let one = vec![vec![1.0, 2.0]];
1222 assert!(matches!(
1223 split_absolute_residual_quantiles(
1224 &one,
1225 &[0.9],
1226 ConformalMultiTargetPolicy::Marginal,
1227 ConformalSmallSamplePolicy::Error,
1228 ),
1229 Err(ConformalError::SmallSampleRank {
1230 rank: 2,
1231 sample_count: 1
1232 })
1233 ));
1234 let unbounded = split_absolute_residual_quantiles(
1235 &one,
1236 &[0.9],
1237 ConformalMultiTargetPolicy::Marginal,
1238 ConformalSmallSamplePolicy::Unbounded,
1239 )
1240 .unwrap();
1241 assert_eq!(
1242 unbounded[0].radii,
1243 vec![ConformalRadius::Unbounded, ConformalRadius::Unbounded]
1244 );
1245 }
1246
1247 #[test]
1248 fn frozen_w0_unsorted_residual_quantiles_match() {
1249 let residuals = [
1250 0.4, 1.0, 0.2, 1.8, 0.6, 1.2, 0.8, 2.0, 0.1, 1.6, 0.3, 1.4, 0.5, 1.9, 0.7, 1.1, 0.9,
1251 1.3, 1.5, 1.7,
1252 ]
1253 .into_iter()
1254 .map(|value| vec![value])
1255 .collect::<Vec<_>>();
1256 let quantiles = split_absolute_residual_quantiles(
1257 &residuals,
1258 &[0.9, 0.95],
1259 ConformalMultiTargetPolicy::Marginal,
1260 ConformalSmallSamplePolicy::Error,
1261 )
1262 .unwrap();
1263 assert_eq!(quantiles[0].rank, 19);
1264 assert_eq!(quantiles[0].radii, vec![finite(1.9)]);
1265 assert_eq!(quantiles[1].rank, 20);
1266 assert_eq!(quantiles[1].radii, vec![finite(2.0)]);
1267 }
1268
1269 #[test]
1270 fn marginal_and_joint_max_calibration_are_distinct() {
1271 let residuals = vec![
1272 vec![1.0, 4.0],
1273 vec![2.0, 1.0],
1274 vec![3.0, 3.0],
1275 vec![4.0, 2.0],
1276 ];
1277 let marginal = split_absolute_residual_quantiles(
1278 &residuals,
1279 &[0.5],
1280 ConformalMultiTargetPolicy::Marginal,
1281 ConformalSmallSamplePolicy::Error,
1282 )
1283 .unwrap();
1284 assert_eq!(marginal[0].radii, vec![finite(3.0), finite(3.0)]);
1285
1286 let joint = split_absolute_residual_quantiles(
1287 &residuals,
1288 &[0.5],
1289 ConformalMultiTargetPolicy::JointMax,
1290 ConformalSmallSamplePolicy::Error,
1291 )
1292 .unwrap();
1293 assert_eq!(joint[0].radii, vec![finite(4.0)]);
1294 }
1295
1296 #[test]
1297 fn multi_coverage_application_is_nested_and_preserves_midpoints() {
1298 let residuals = (1..=20)
1299 .map(|value| vec![f64::from(value), f64::from(value) / 2.0])
1300 .collect::<Vec<_>>();
1301 let quantiles = split_absolute_residual_quantiles(
1302 &residuals,
1303 &[0.8, 0.9, 0.95, 0.99],
1304 ConformalMultiTargetPolicy::Marginal,
1305 ConformalSmallSamplePolicy::Unbounded,
1306 )
1307 .unwrap();
1308 let points = vec![vec![100.0, 10.0], vec![200.0, 20.0]];
1309 let intervals = apply_split_absolute_residual(
1310 &points,
1311 &quantiles,
1312 ConformalMultiTargetPolicy::Marginal,
1313 )
1314 .unwrap();
1315 assert_eq!(intervals.len(), 4);
1316 assert_eq!(intervals[0].cells[0][0], finite_cell(83.0, 117.0));
1317 assert_eq!(intervals[1].cells[0][0], finite_cell(81.0, 119.0));
1318 assert_eq!(intervals[2].cells[0][0], finite_cell(80.0, 120.0));
1319 assert_eq!(intervals[3].cells[0][0], RegressionIntervalCell::Unbounded);
1320 assert_eq!(intervals[0].cells[0][0].midpoint(), Some(100.0));
1321 assert_eq!(intervals[3].cells[0][0].endpoints(), (None, None));
1322 }
1323
1324 #[test]
1325 fn joint_radius_expands_to_every_prediction_target() {
1326 let quantiles = vec![SplitConformalQuantile {
1327 coverage: 0.8,
1328 rank: 4,
1329 radii: vec![finite(2.0)],
1330 }];
1331 let intervals = apply_split_absolute_residual(
1332 &[vec![10.0, 20.0]],
1333 &quantiles,
1334 ConformalMultiTargetPolicy::JointMax,
1335 )
1336 .unwrap();
1337 assert_eq!(
1338 intervals[0].cells[0],
1339 vec![finite_cell(8.0, 12.0), finite_cell(18.0, 22.0)]
1340 );
1341 }
1342
1343 #[test]
1344 fn decimal_endpoints_match_w0_instead_of_binary64_intermediate_arithmetic() {
1345 let quantiles = vec![SplitConformalQuantile {
1346 coverage: 0.8,
1347 rank: 1,
1348 radii: vec![finite(0.2)],
1349 }];
1350 let interval = apply_split_absolute_residual(
1351 &[vec![0.1]],
1352 &quantiles,
1353 ConformalMultiTargetPolicy::Marginal,
1354 )
1355 .unwrap();
1356 let RegressionIntervalCell::Finite { lower, upper } = interval[0].cells[0][0] else {
1357 panic!("W0 decimal endpoints must be finite");
1358 };
1359 assert_eq!(lower.to_bits(), (-0.1_f64).to_bits());
1360 assert_eq!(upper.to_bits(), 0.3_f64.to_bits());
1361 assert_ne!(upper.to_bits(), (0.1_f64 + 0.2_f64).to_bits());
1362 assert!(decimal_interval_closes(0.1, 0.2, lower, upper).unwrap());
1363
1364 let (lower, upper) = decimal_conformal_endpoints(100.0, 90.0).unwrap();
1365 assert_eq!((lower, upper), (10.0, 190.0));
1366 assert!(decimal_interval_closes(100.0, 90.0, lower, upper).unwrap());
1367 let (lower, upper) = decimal_conformal_endpoints(-100.0, 90.0).unwrap();
1368 assert_eq!((lower, upper), (-190.0, -10.0));
1369 assert!(decimal_interval_closes(-100.0, 90.0, lower, upper).unwrap());
1370 }
1371
1372 #[test]
1373 fn decimal_endpoints_reject_a_radius_below_the_point_ulp() {
1374 let quantiles = vec![SplitConformalQuantile {
1375 coverage: 0.8,
1376 rank: 1,
1377 radii: vec![finite(0.5)],
1378 }];
1379 assert!(matches!(
1380 apply_split_absolute_residual(
1381 &[vec![1.0e16]],
1382 &quantiles,
1383 ConformalMultiTargetPolicy::Marginal,
1384 ),
1385 Err(ConformalError::UnrepresentableInterval {
1386 coverage_index: 0,
1387 row: 0,
1388 target: 0,
1389 })
1390 ));
1391 }
1392
1393 #[test]
1394 fn decimal_endpoints_preserve_signed_zero_and_minimum_subnormal() {
1395 let zero_quantile = vec![SplitConformalQuantile {
1396 coverage: 0.5,
1397 rank: 1,
1398 radii: vec![finite(-0.0)],
1399 }];
1400 let zero_interval = apply_split_absolute_residual(
1401 &[vec![-0.0]],
1402 &zero_quantile,
1403 ConformalMultiTargetPolicy::Marginal,
1404 )
1405 .unwrap();
1406 let RegressionIntervalCell::Finite { lower, upper } = zero_interval[0].cells[0][0] else {
1407 panic!("zero-radius interval must be finite");
1408 };
1409 assert_eq!(lower.to_bits(), (-0.0_f64).to_bits());
1410 assert_eq!(upper.to_bits(), 0.0_f64.to_bits());
1411 assert!(decimal_interval_closes(-0.0, 0.0, lower, upper).unwrap());
1412
1413 let minimum_subnormal = f64::from_bits(1);
1414 let subnormal_quantile = vec![SplitConformalQuantile {
1415 coverage: 0.5,
1416 rank: 1,
1417 radii: vec![finite(minimum_subnormal)],
1418 }];
1419 let subnormal_interval = apply_split_absolute_residual(
1420 &[vec![0.0], vec![minimum_subnormal]],
1421 &subnormal_quantile,
1422 ConformalMultiTargetPolicy::Marginal,
1423 )
1424 .unwrap();
1425 assert_eq!(
1426 subnormal_interval[0].cells[0][0],
1427 finite_cell(-minimum_subnormal, minimum_subnormal)
1428 );
1429 assert_eq!(
1430 subnormal_interval[0].cells[1][0],
1431 finite_cell(0.0, f64::from_bits(2))
1432 );
1433 for (point, cell) in [0.0, minimum_subnormal]
1434 .into_iter()
1435 .zip(&subnormal_interval[0].cells)
1436 {
1437 let RegressionIntervalCell::Finite { lower, upper } = cell[0] else {
1438 panic!("subnormal interval must be finite");
1439 };
1440 assert!(decimal_interval_closes(point, minimum_subnormal, lower, upper).unwrap());
1441 }
1442 }
1443
1444 #[test]
1445 fn midpoint_is_stable_for_subnormal_and_extreme_bounds() {
1446 let minimum_subnormal = f64::from_bits(1);
1447 assert_eq!(
1448 finite_cell(minimum_subnormal, minimum_subnormal).midpoint(),
1449 Some(minimum_subnormal)
1450 );
1451 assert_eq!(finite_cell(f64::MAX, f64::MAX).midpoint(), Some(f64::MAX));
1452 assert_eq!(finite_cell(-f64::MAX, f64::MAX).midpoint(), Some(0.0));
1453 assert_eq!(
1454 finite_cell(-minimum_subnormal, 0.0)
1455 .midpoint()
1456 .unwrap()
1457 .to_bits(),
1458 (-0.0_f64).to_bits()
1459 );
1460 assert_eq!(
1461 finite_cell(minimum_subnormal, f64::from_bits(2)).midpoint(),
1462 Some(f64::from_bits(2))
1463 );
1464 }
1465
1466 #[test]
1467 fn finite_metrics_match_marginal_and_joint_w0_semantics() {
1468 let truth = vec![vec![1.0, 10.0], vec![3.0, 20.0]];
1469 let interval = RegressionConformalInterval {
1470 coverage: 0.8,
1471 cells: vec![
1472 vec![finite_cell(0.0, 2.0), finite_cell(9.0, 11.0)],
1473 vec![finite_cell(0.0, 2.0), finite_cell(19.0, 21.0)],
1474 ],
1475 };
1476 let marginal =
1477 regression_conformal_metrics(&truth, &interval, ConformalMultiTargetPolicy::Marginal)
1478 .unwrap();
1479 assert_eq!(marginal.len(), 2);
1480 assert_close(marginal[0].empirical_coverage, 0.5);
1481 assert_close(marginal[0].coverage_gap, -0.3);
1482 assert_close(marginal[0].mean_width.unwrap(), 2.0);
1483 assert_close(marginal[0].median_width.unwrap(), 2.0);
1484 assert_close(marginal[0].interval_score.unwrap(), 7.0);
1485 assert_close(marginal[1].empirical_coverage, 1.0);
1486 assert_close(marginal[1].interval_score.unwrap(), 2.0);
1487
1488 let joint =
1489 regression_conformal_metrics(&truth, &interval, ConformalMultiTargetPolicy::JointMax)
1490 .unwrap();
1491 assert_eq!(joint.len(), 1);
1492 assert_eq!(joint[0].target_index, None);
1493 assert_close(joint[0].empirical_coverage, 0.5);
1494 assert_close(joint[0].mean_width.unwrap(), 2.0);
1495 assert_close(joint[0].median_width.unwrap(), 2.0);
1496 assert_close(joint[0].interval_score.unwrap(), 4.5);
1497 }
1498
1499 #[test]
1500 fn frozen_w0_prediction_blocks_and_metrics_match() {
1501 let points = vec![vec![10.0, 20.0], vec![11.0, 21.0]];
1502 let truth = vec![vec![10.0, 21.0], vec![14.0, 21.0]];
1503
1504 let marginal_quantiles = vec![
1505 SplitConformalQuantile {
1506 coverage: 0.8,
1507 rank: 1,
1508 radii: vec![finite(2.0), finite(2.0)],
1509 },
1510 SplitConformalQuantile {
1511 coverage: 0.9,
1512 rank: 2,
1513 radii: vec![finite(3.0), finite(3.0)],
1514 },
1515 ];
1516 let marginal_intervals = apply_split_absolute_residual(
1517 &points,
1518 &marginal_quantiles,
1519 ConformalMultiTargetPolicy::Marginal,
1520 )
1521 .unwrap();
1522 assert_eq!(
1523 marginal_intervals[0].cells,
1524 vec![
1525 vec![finite_cell(8.0, 12.0), finite_cell(18.0, 22.0)],
1526 vec![finite_cell(9.0, 13.0), finite_cell(19.0, 23.0)],
1527 ]
1528 );
1529 let marginal_80 = regression_conformal_metrics(
1530 &truth,
1531 &marginal_intervals[0],
1532 ConformalMultiTargetPolicy::Marginal,
1533 )
1534 .unwrap();
1535 assert_close(marginal_80[0].empirical_coverage, 0.5);
1536 assert_close(marginal_80[0].coverage_gap, -0.300_000_000_000_000_04);
1537 assert_close(marginal_80[0].mean_width.unwrap(), 4.0);
1538 assert_close(marginal_80[0].median_width.unwrap(), 4.0);
1539 assert_close(marginal_80[0].interval_score.unwrap(), 9.0);
1540 assert_close(marginal_80[1].empirical_coverage, 1.0);
1541 assert_close(marginal_80[1].interval_score.unwrap(), 4.0);
1542 let marginal_90 = regression_conformal_metrics(
1543 &truth,
1544 &marginal_intervals[1],
1545 ConformalMultiTargetPolicy::Marginal,
1546 )
1547 .unwrap();
1548 for metric in marginal_90 {
1549 assert_close(metric.empirical_coverage, 1.0);
1550 assert_close(metric.mean_width.unwrap(), 6.0);
1551 assert_close(metric.interval_score.unwrap(), 6.0);
1552 }
1553
1554 let joint_quantiles = vec![
1555 SplitConformalQuantile {
1556 coverage: 0.8,
1557 rank: 1,
1558 radii: vec![finite(3.0)],
1559 },
1560 SplitConformalQuantile {
1561 coverage: 0.9,
1562 rank: 2,
1563 radii: vec![finite(4.0)],
1564 },
1565 ];
1566 let joint_intervals = apply_split_absolute_residual(
1567 &points,
1568 &joint_quantiles,
1569 ConformalMultiTargetPolicy::JointMax,
1570 )
1571 .unwrap();
1572 for (interval, expected_width) in joint_intervals.iter().zip([6.0, 8.0]) {
1573 let metric = regression_conformal_metrics(
1574 &truth,
1575 interval,
1576 ConformalMultiTargetPolicy::JointMax,
1577 )
1578 .unwrap();
1579 assert_close(metric[0].empirical_coverage, 1.0);
1580 assert_close(metric[0].mean_width.unwrap(), expected_width);
1581 assert_close(metric[0].median_width.unwrap(), expected_width);
1582 assert_close(metric[0].interval_score.unwrap(), expected_width);
1583 }
1584 }
1585
1586 #[test]
1587 fn unbounded_metrics_keep_coverage_and_tag_measurements_unavailable() {
1588 let truth = vec![vec![1.0, 10.0], vec![3.0, 20.0]];
1589 let interval = RegressionConformalInterval {
1590 coverage: 0.9,
1591 cells: vec![
1592 vec![RegressionIntervalCell::Unbounded, finite_cell(9.0, 11.0)],
1593 vec![RegressionIntervalCell::Unbounded, finite_cell(19.0, 21.0)],
1594 ],
1595 };
1596 let marginal =
1597 regression_conformal_metrics(&truth, &interval, ConformalMultiTargetPolicy::Marginal)
1598 .unwrap();
1599 assert_eq!(
1600 marginal[0].measurement_status,
1601 ConformalMeasurementStatus::Unbounded
1602 );
1603 assert_eq!(marginal[0].empirical_coverage, 1.0);
1604 assert_eq!(marginal[0].mean_width, None);
1605 assert_eq!(
1606 marginal[1].measurement_status,
1607 ConformalMeasurementStatus::Finite
1608 );
1609
1610 let joint =
1611 regression_conformal_metrics(&truth, &interval, ConformalMultiTargetPolicy::JointMax)
1612 .unwrap();
1613 assert_eq!(
1614 joint[0].measurement_status,
1615 ConformalMeasurementStatus::Unbounded
1616 );
1617 assert_eq!(joint[0].empirical_coverage, 1.0);
1618 assert_eq!(joint[0].interval_score, None);
1619 }
1620
1621 #[test]
1622 fn finite_metric_means_do_not_overflow_when_the_mean_is_representable() {
1623 let interval = RegressionConformalInterval {
1624 coverage: 0.5,
1625 cells: vec![
1626 vec![finite_cell(0.0, f64::MAX)],
1627 vec![finite_cell(0.0, f64::MAX)],
1628 ],
1629 };
1630 let metrics = regression_conformal_metrics(
1631 &[vec![0.0], vec![f64::MAX]],
1632 &interval,
1633 ConformalMultiTargetPolicy::Marginal,
1634 )
1635 .unwrap();
1636 assert_eq!(metrics[0].mean_width, Some(f64::MAX));
1637 assert_eq!(metrics[0].median_width, Some(f64::MAX));
1638 assert_eq!(metrics[0].interval_score, Some(f64::MAX));
1639 }
1640
1641 #[test]
1642 fn finite_metric_mean_keeps_w0_sequential_sum_rounding() {
1643 let values = [1.0, 1.0, 1.0e16];
1644 let mut sequential_sum = 0.0;
1645 for value in values {
1646 sequential_sum += value;
1647 }
1648 assert_eq!(sequential_sum / 3.0, 3_333_333_333_333_334.0);
1649 assert_eq!(
1650 checked_mean(&values, "rounding parity").unwrap(),
1651 sequential_sum / 3.0
1652 );
1653 }
1654
1655 #[test]
1656 fn invalid_coverages_and_residual_matrices_are_rejected() {
1657 for coverages in [
1658 vec![],
1659 vec![0.0],
1660 vec![1.0],
1661 vec![f64::NAN],
1662 vec![0.9, 0.8],
1663 vec![0.9, 0.9],
1664 ] {
1665 assert!(validate_conformal_coverages(&coverages).is_err());
1666 }
1667 let invalid = [
1668 vec![],
1669 vec![vec![]],
1670 vec![vec![1.0], vec![1.0, 2.0]],
1671 vec![vec![f64::INFINITY]],
1672 vec![vec![-1.0]],
1673 ];
1674 for residuals in invalid {
1675 assert!(split_absolute_residual_quantiles(
1676 &residuals,
1677 &[0.5],
1678 ConformalMultiTargetPolicy::Marginal,
1679 ConformalSmallSamplePolicy::Error,
1680 )
1681 .is_err());
1682 }
1683 }
1684
1685 #[test]
1686 fn application_rejects_bad_shape_status_order_and_overflow() {
1687 let points = vec![vec![1.0, 2.0]];
1688 let bad_shape = vec![SplitConformalQuantile {
1689 coverage: 0.8,
1690 rank: 2,
1691 radii: vec![finite(1.0)],
1692 }];
1693 assert!(apply_split_absolute_residual(
1694 &points,
1695 &bad_shape,
1696 ConformalMultiTargetPolicy::Marginal
1697 )
1698 .is_err());
1699
1700 let mixed = vec![SplitConformalQuantile {
1701 coverage: 0.8,
1702 rank: 2,
1703 radii: vec![finite(1.0), ConformalRadius::Unbounded],
1704 }];
1705 assert!(matches!(
1706 apply_split_absolute_residual(&points, &mixed, ConformalMultiTargetPolicy::Marginal),
1707 Err(ConformalError::MixedRadiusStatus { .. })
1708 ));
1709
1710 let non_nested = vec![
1711 SplitConformalQuantile {
1712 coverage: 0.8,
1713 rank: 2,
1714 radii: vec![finite(2.0), finite(2.0)],
1715 },
1716 SplitConformalQuantile {
1717 coverage: 0.9,
1718 rank: 3,
1719 radii: vec![finite(1.0), finite(2.0)],
1720 },
1721 ];
1722 assert!(matches!(
1723 apply_split_absolute_residual(
1724 &points,
1725 &non_nested,
1726 ConformalMultiTargetPolicy::Marginal
1727 ),
1728 Err(ConformalError::NonNestedRadius { .. })
1729 ));
1730
1731 let overflow = vec![SplitConformalQuantile {
1732 coverage: 0.8,
1733 rank: 2,
1734 radii: vec![finite(f64::MAX)],
1735 }];
1736 assert!(matches!(
1737 apply_split_absolute_residual(
1738 &[vec![f64::MAX]],
1739 &overflow,
1740 ConformalMultiTargetPolicy::Marginal
1741 ),
1742 Err(ConformalError::ArithmeticOverflow { .. })
1743 ));
1744 }
1745
1746 #[test]
1747 fn metrics_reject_invalid_shapes_bounds_and_nonfinite_truth() {
1748 let valid_interval = RegressionConformalInterval {
1749 coverage: 0.8,
1750 cells: vec![vec![finite_cell(0.0, 2.0)]],
1751 };
1752 assert!(regression_conformal_metrics(
1753 &[vec![f64::NAN]],
1754 &valid_interval,
1755 ConformalMultiTargetPolicy::Marginal
1756 )
1757 .is_err());
1758 assert!(regression_conformal_metrics(
1759 &[vec![1.0], vec![2.0]],
1760 &valid_interval,
1761 ConformalMultiTargetPolicy::Marginal
1762 )
1763 .is_err());
1764 let bad_bounds = RegressionConformalInterval {
1765 coverage: 0.8,
1766 cells: vec![vec![finite_cell(2.0, 1.0)]],
1767 };
1768 assert!(matches!(
1769 regression_conformal_metrics(
1770 &[vec![1.0]],
1771 &bad_bounds,
1772 ConformalMultiTargetPolicy::Marginal
1773 ),
1774 Err(ConformalError::InvalidIntervalCell { .. })
1775 ));
1776 }
1777}