Skip to main content

dag_ml_core/
conformal.rs

1//! Native split-conformal regression kernels.
2//!
3//! This module implements split absolute-residual calibration, interval
4//! application, and interval metrics without owning predictors or feature
5//! buffers. It is intentionally a typed Rust surface only: persistence and
6//! binding contracts are added separately once their wire formats are frozen.
7
8use thiserror::Error;
9
10/// Largest calibration sample count accepted by the exact rank routine.
11///
12/// The rank convention uses `n + 1`, so `u64::MAX` itself is excluded. The
13/// routine performs the remaining decimal arithmetic in `u128` and returns a
14/// rank in `1..=n+1` without binary64 multiplication.
15pub 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/// Multi-target nonconformity reduction for split regression intervals.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum ConformalMultiTargetPolicy {
24    /// Calibrate one absolute-residual quantile per target.
25    Marginal,
26    /// Reduce every calibration row to its maximum target residual.
27    JointMax,
28}
29
30/// Behavior when `ceil((n + 1) * coverage)` exceeds `n`.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum ConformalSmallSamplePolicy {
33    /// Refuse calibration because no finite order statistic exists.
34    Error,
35    /// Emit a tagged unbounded radius; never synthesize an infinite sentinel.
36    Unbounded,
37}
38
39/// One calibrated radius.
40#[derive(Clone, Copy, Debug, PartialEq)]
41pub enum ConformalRadius {
42    Finite(f64),
43    Unbounded,
44}
45
46/// Quantile record for one requested coverage.
47#[derive(Clone, Debug, PartialEq)]
48pub struct SplitConformalQuantile {
49    pub coverage: f64,
50    /// One-indexed finite-sample rank. It may equal `n + 1` for an unbounded
51    /// small-sample record.
52    pub rank: u64,
53    /// Per-target radii for [`ConformalMultiTargetPolicy::Marginal`], or one
54    /// shared radius for [`ConformalMultiTargetPolicy::JointMax`].
55    pub radii: Vec<ConformalRadius>,
56}
57
58/// One regression interval cell.
59///
60/// The tagged form makes unbounded endpoints inseparable, avoiding `(-inf,
61/// +inf)` sentinels and half-unbounded states.
62#[derive(Clone, Copy, Debug, PartialEq)]
63pub enum RegressionIntervalCell {
64    Finite { lower: f64, upper: f64 },
65    Unbounded,
66}
67
68impl RegressionIntervalCell {
69    /// Return wire-shaped paired endpoints (`None, None` when unbounded).
70    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    /// Return a finite midpoint without overflowing endpoint addition.
78    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/// Multi-row, multi-target interval at one coverage.
87#[derive(Clone, Debug, PartialEq)]
88pub struct RegressionConformalInterval {
89    pub coverage: f64,
90    pub cells: Vec<Vec<RegressionIntervalCell>>,
91}
92
93/// Availability of finite width and interval-score summaries.
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum ConformalMeasurementStatus {
96    Finite,
97    Unbounded,
98}
99
100/// Reconstructed regression interval metrics for one coverage and target scope.
101#[derive(Clone, Debug, PartialEq)]
102pub struct RegressionConformalMetrics {
103    /// Target column for marginal metrics; `None` for a joint-max summary.
104    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/// Validation or finite-arithmetic failure in the native conformal kernels.
114#[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
224/// Validate a non-empty, strictly increasing list of finite binary64 coverages.
225pub 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
240/// Compute `ceil((n + 1) * coverage)` from the binary64 shortest decimal.
241///
242/// Rust and Python may choose fixed versus exponent notation differently, but
243/// their shortest-roundtrip renderings denote the same exact decimal. Parsing
244/// that decimal into integer arithmetic reproduces Python's
245/// `Decimal(repr(coverage))` convention and avoids a binary64 multiplication
246/// near integer rank boundaries.
247pub 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    // A binary64 shortest coefficient has at most 17 significant decimal
269    // digits. With n <= u64::MAX - 1, scaled < 10^37; any denominator >= 10^39
270    // therefore yields the exact positive ceiling 1 without materializing the
271    // (potentially 10^324) denominator.
272    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
284/// Calibrate split absolute-residual radii for every requested coverage.
285///
286/// Negative-zero residuals are normalized to positive zero before ordering so
287/// equal scores have one deterministic binary64 representation.
288pub 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
363/// Apply calibrated radii to a finite multi-target prediction matrix.
364///
365/// Quantile coverages and ranks must be ordered, and radii must be nested. A
366/// finite bound overflow is rejected rather than converted into an unbounded
367/// interval, preserving the distinction between arithmetic failure and the
368/// explicit small-sample policy. Finite endpoints are the correctly rounded
369/// binary64 conversions of `Decimal(repr(point)) +/- Decimal(repr(radius))`,
370/// as frozen by W0. The result is rejected when binary64 endpoints cannot
371/// preserve both the W0 decimal midpoint and radius closures exactly.
372pub 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
417/// Reconstruct regression coverage, width, and Winkler interval score.
418///
419/// Marginal mode returns one record per target. Joint-max mode counts a row as
420/// covered only when all targets are covered, while widths and scores are
421/// flattened in row-major `(row, target)` order. If any summarized cell is
422/// unbounded, coverage remains available but width and score fields are tagged
423/// unavailable.
424pub 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/// Exact finite decimal recovered from a binary64 shortest-roundtrip token.
530///
531/// `digits * 10^exponent` is stored independently of binary floating-point so
532/// W0 endpoint arithmetic never performs an intermediate binary64 operation.
533#[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    // Decimal's signed-zero results for the only zero-radius ambiguity:
823    // `-0 - +0` remains negative, while `-0 + +0` is positive.
824    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        // This is the frozen W0 order-sensitive `sum(values) / len(values)`
1096        // path. The online fallback below is used only when that sum overflows
1097        // even though its mathematical mean can remain representable.
1098        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}