Skip to main content

vortex_array/expr/stats/
precision.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Debug;
5use std::fmt::Display;
6use std::fmt::Formatter;
7
8use vortex_error::VortexExpect;
9
10use crate::dtype::DType;
11use crate::expr::stats::IntersectionResult;
12use crate::expr::stats::StatBound;
13use crate::expr::stats::StatType;
14use crate::partial_ord::partial_min;
15use crate::scalar::Scalar;
16use crate::scalar::ScalarValue;
17
18/// A statistic has a precision `Exact` or `Inexact`. This represents uncertainty in that value.
19/// Exact values are computed, where can inexact values are likely inferred from compute functions.
20///
21/// Inexact statistics form a range of possible values that the statistic could be.
22/// This is statistic specific, for max this will be an upper bound. Meaning that the actual max
23/// in an array is guaranteed to be less than or equal to the inexact value, but equal to the exact
24/// value.
25#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
26pub enum Precision<T> {
27    Exact(T),
28    Inexact(T),
29    #[default]
30    Absent,
31}
32
33impl<T, E> Precision<Result<T, E>> {
34    /// Transpose a `Precision<Result<T, E>>` into a `Result<Precision<T>, E>`.
35    pub fn transpose(self) -> Result<Precision<T>, E> {
36        match self {
37            Self::Exact(value) => value.map(Precision::Exact),
38            Self::Inexact(value) => value.map(Precision::Inexact),
39            Self::Absent => Ok(Precision::Absent),
40        }
41    }
42}
43
44impl<T> Precision<T>
45where
46    T: Copy,
47{
48    pub fn to_inexact(&self) -> Self {
49        use Precision::*;
50
51        match self {
52            Exact(v) | Inexact(v) => Inexact(*v),
53            Absent => Absent,
54        }
55    }
56}
57
58impl<T> Precision<T> {
59    /// Creates an exact value
60    pub fn exact<S: Into<T>>(s: S) -> Precision<T> {
61        Self::Exact(s.into())
62    }
63
64    /// Creates an inexact value
65    pub fn inexact<S: Into<T>>(s: S) -> Precision<T> {
66        Self::Inexact(s.into())
67    }
68
69    /// Pushed the ref into the Precision enum
70    pub fn as_ref(&self) -> Precision<&T> {
71        use Precision::*;
72
73        match self {
74            Exact(val) => Exact(val),
75            Inexact(val) => Inexact(val),
76            Absent => Absent,
77        }
78    }
79
80    /// Converts `self` into an inexact bound
81    pub fn into_inexact(self) -> Self {
82        use Precision::*;
83
84        match self {
85            Exact(v) | Inexact(v) => Inexact(v),
86            Absent => Absent,
87        }
88    }
89
90    /// Returns the exact value from the bound, if that value is inexact, otherwise `None`.
91    pub fn as_exact(self) -> Option<T> {
92        match self {
93            Self::Exact(val) => Some(val),
94            _ => None,
95        }
96    }
97
98    /// Returns the exact value from the bound, if that value is inexact, otherwise `None`.
99    pub fn as_inexact(self) -> Option<T> {
100        match self {
101            Self::Inexact(val) => Some(val),
102            _ => None,
103        }
104    }
105
106    /// Returns true when representing an exact value.
107    pub fn is_exact(&self) -> bool {
108        matches!(self, Self::Exact(_))
109    }
110
111    /// Returns true when representing an absent value
112    pub fn is_absent(&self) -> bool {
113        matches!(self, Self::Absent)
114    }
115
116    /// Map the value of either precision value
117    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Precision<U> {
118        use Precision::*;
119
120        match self {
121            Exact(value) => Exact(f(value)),
122            Inexact(value) => Inexact(f(value)),
123            Absent => Absent,
124        }
125    }
126
127    pub fn and_then<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Precision<U> {
128        use Precision::*;
129
130        match self {
131            Exact(value) => match f(value) {
132                Some(v) => Exact(v),
133                None => Absent,
134            },
135            Inexact(value) => match f(value) {
136                Some(v) => Inexact(v),
137                None => Absent,
138            },
139            Absent => Absent,
140        }
141    }
142
143    /// Zip two `Precision` values into a tuple, keeping the inexactness if any.
144    pub fn zip<U>(self, other: Precision<U>) -> Precision<(T, U)> {
145        use Precision::*;
146
147        match (self, other) {
148            (Exact(lhs), Exact(rhs)) => Exact((lhs, rhs)),
149            (Inexact(lhs), Exact(rhs))
150            | (Exact(lhs), Inexact(rhs))
151            | (Inexact(lhs), Inexact(rhs)) => Inexact((lhs, rhs)),
152            (Absent, _) | (_, Absent) => Absent,
153        }
154    }
155
156    /// Unwrap the underlying value
157    pub fn into_inner(self) -> Option<T> {
158        use Precision::*;
159
160        match self {
161            Exact(val) | Inexact(val) => Some(val),
162            Absent => None,
163        }
164    }
165}
166
167impl<T: Display> Display for Precision<T> {
168    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
169        use Precision::*;
170
171        match self {
172            Exact(v) => {
173                write!(f, "{v}")
174            }
175            Inexact(v) => {
176                write!(f, "~{v}")
177            }
178            Absent => {
179                write!(f, "{{empty}}")
180            }
181        }
182    }
183}
184
185impl<T: PartialEq> PartialEq<T> for Precision<T> {
186    fn eq(&self, other: &T) -> bool {
187        match self {
188            Self::Exact(v) => v == other,
189            _ => false,
190        }
191    }
192}
193
194impl Precision<ScalarValue> {
195    /// Convert this [`Precision<ScalarValue>`] into a [`Precision<Scalar>`] with the given
196    /// [`DType`].
197    pub fn into_scalar(self, dtype: DType) -> Precision<Scalar> {
198        self.map(|v| {
199            Scalar::try_new(dtype, Some(v)).vortex_expect("`Precision<ScalarValue>` was invalid")
200        })
201    }
202}
203
204impl Precision<&ScalarValue> {
205    /// Convert this [`Precision<&ScalarValue>`] into a [`Precision<Scalar>`] with the given
206    /// [`DType`].
207    pub fn into_scalar(self, dtype: DType) -> Precision<Scalar> {
208        self.map(|v| {
209            Scalar::try_new(dtype, Some(v.clone()))
210                .vortex_expect("`Precision<ScalarValue>` was invalid")
211        })
212    }
213}
214
215/// This allows a stat with a `Precision` to be interpreted as a bound.
216impl<T> Precision<T> {
217    /// Applied the stat associated bound to the precision value
218    pub fn bound<S: StatType<T>>(self) -> Option<S::Bound> {
219        if self.is_absent() {
220            None
221        } else {
222            Some(S::Bound::lift(self))
223        }
224    }
225}
226
227impl<T: PartialOrd + Clone> StatBound<T> for Precision<T> {
228    fn lift(value: Precision<T>) -> Self {
229        value
230    }
231
232    fn into_value(self) -> Precision<T> {
233        self
234    }
235
236    fn union(&self, other: &Self) -> Option<Self> {
237        match self
238            .clone()
239            .zip(other.clone())
240            .map(|(lhs, rhs)| partial_min(&lhs, &rhs).cloned())
241        {
242            Precision::Exact(v) => Some(Precision::Exact(v?)),
243            Precision::Inexact(v) => Some(Precision::Inexact(v?)),
244            Precision::Absent => None,
245        }
246    }
247
248    fn intersection(&self, other: &Self) -> Option<IntersectionResult<Self>> {
249        Some(match (self, other) {
250            (Precision::Exact(lhs), Precision::Exact(rhs)) => {
251                if lhs.partial_cmp(rhs)?.is_eq() {
252                    IntersectionResult::Value(Precision::Exact(lhs.clone()))
253                } else {
254                    IntersectionResult::Empty
255                }
256            }
257            (Precision::Exact(exact), Precision::Inexact(inexact))
258            | (Precision::Inexact(inexact), Precision::Exact(exact)) => {
259                if exact.partial_cmp(inexact)?.is_lt() {
260                    IntersectionResult::Value(Precision::Inexact(exact.clone()))
261                } else {
262                    IntersectionResult::Value(Precision::Exact(exact.clone()))
263                }
264            }
265            (Precision::Inexact(lhs), Precision::Inexact(rhs)) => {
266                IntersectionResult::Value(Precision::Inexact(partial_min(lhs, rhs)?.clone()))
267            }
268            (_, Precision::Absent) | (Precision::Absent, _) => IntersectionResult::Empty,
269        })
270    }
271
272    fn to_exact(&self) -> Option<&T> {
273        match self {
274            Precision::Exact(val) => Some(val),
275            _ => None,
276        }
277    }
278}