vortex_array/compute/
is_constant.rs1use std::any::Any;
2use std::sync::LazyLock;
3
4use arcref::ArcRef;
5use vortex_dtype::{DType, Nullability};
6use vortex_error::{VortexError, VortexResult, vortex_bail, vortex_err};
7use vortex_scalar::Scalar;
8
9use crate::Array;
10use crate::arrays::{ConstantVTable, ListVTable, NullVTable};
11use crate::compute::{ComputeFn, ComputeFnVTable, InvocationArgs, Kernel, Options, Output};
12use crate::stats::{Precision, Stat, StatsProviderExt};
13use crate::vtable::VTable;
14
15pub fn is_constant(array: &dyn Array) -> VortexResult<Option<bool>> {
30    let opts = IsConstantOpts::default();
31    is_constant_opts(array, &opts)
32}
33
34pub fn is_constant_opts(array: &dyn Array, options: &IsConstantOpts) -> VortexResult<Option<bool>> {
38    let result = IS_CONSTANT_FN
39        .invoke(&InvocationArgs {
40            inputs: &[array.into()],
41            options,
42        })?
43        .unwrap_scalar()?
44        .as_bool()
45        .value();
46
47    Ok(result)
48}
49
50pub static IS_CONSTANT_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
51    let compute = ComputeFn::new("is_constant".into(), ArcRef::new_ref(&IsConstant));
52    for kernel in inventory::iter::<IsConstantKernelRef> {
53        compute.register_kernel(kernel.0.clone());
54    }
55    compute
56});
57
58struct IsConstant;
59
60impl ComputeFnVTable for IsConstant {
61    fn invoke(
62        &self,
63        args: &InvocationArgs,
64        kernels: &[ArcRef<dyn Kernel>],
65    ) -> VortexResult<Output> {
66        let IsConstantArgs { array, options } = IsConstantArgs::try_from(args)?;
67
68        if let Some(Precision::Exact(value)) = array.statistics().get_as::<bool>(Stat::IsConstant) {
70            return Ok(Scalar::from(Some(value)).into());
71        }
72
73        let value = is_constant_impl(array, options, kernels)?;
74
75        if options.cost == Cost::Canonicalize && !array.is::<ListVTable>() {
77            assert!(
79                value.is_some(),
80                "is constant in array {array} canonicalize returned None"
81            );
82        }
83
84        if let Some(value) = value {
86            array
87                .statistics()
88                .set(Stat::IsConstant, Precision::Exact(value.into()));
89        }
90
91        Ok(Scalar::from(value).into())
92    }
93
94    fn return_dtype(&self, _args: &InvocationArgs) -> VortexResult<DType> {
95        Ok(DType::Bool(Nullability::Nullable))
98    }
99
100    fn return_len(&self, _args: &InvocationArgs) -> VortexResult<usize> {
101        Ok(1)
102    }
103
104    fn is_elementwise(&self) -> bool {
105        false
106    }
107}
108
109fn is_constant_impl(
110    array: &dyn Array,
111    options: &IsConstantOpts,
112    kernels: &[ArcRef<dyn Kernel>],
113) -> VortexResult<Option<bool>> {
114    match array.len() {
115        0 => return Ok(Some(false)),
117        1 => return Ok(Some(true)),
119        _ => {}
120    }
121
122    if array.as_opt::<ConstantVTable>().is_some() || array.as_opt::<NullVTable>().is_some() {
124        return Ok(Some(true));
125    }
126
127    let all_invalid = array.all_invalid()?;
128    if all_invalid {
129        return Ok(Some(true));
130    }
131
132    let all_valid = array.all_valid()?;
133
134    if !all_valid && !all_invalid {
136        return Ok(Some(false));
137    }
138
139    let min = array
141        .statistics()
142        .get_scalar(Stat::Min, array.dtype())
143        .and_then(|p| p.as_exact());
144    let max = array
145        .statistics()
146        .get_scalar(Stat::Max, array.dtype())
147        .and_then(|p| p.as_exact());
148
149    if let Some((min, max)) = min.zip(max) {
150        if min == max {
151            return Ok(Some(true));
152        }
153    }
154
155    assert!(
156        all_valid,
157        "All values must be valid as an invariant of the VTable."
158    );
159    let args = InvocationArgs {
160        inputs: &[array.into()],
161        options,
162    };
163    for kernel in kernels {
164        if let Some(output) = kernel.invoke(&args)? {
165            return Ok(output.unwrap_scalar()?.as_bool().value());
166        }
167    }
168    if let Some(output) = array.invoke(&IS_CONSTANT_FN, &args)? {
169        return Ok(output.unwrap_scalar()?.as_bool().value());
170    }
171
172    log::debug!(
173        "No is_constant implementation found for {}",
174        array.encoding_id()
175    );
176
177    if options.cost == Cost::Canonicalize && !array.is_canonical() {
178        let array = array.to_canonical()?;
179        let is_constant = is_constant_opts(array.as_ref(), options)?;
180        return Ok(is_constant);
181    }
182
183    Ok(None)
185}
186
187pub struct IsConstantKernelRef(ArcRef<dyn Kernel>);
188inventory::collect!(IsConstantKernelRef);
189
190pub trait IsConstantKernel: VTable {
191    fn is_constant(&self, array: &Self::Array, opts: &IsConstantOpts)
198    -> VortexResult<Option<bool>>;
199}
200
201#[derive(Debug)]
202pub struct IsConstantKernelAdapter<V: VTable>(pub V);
203
204impl<V: VTable + IsConstantKernel> IsConstantKernelAdapter<V> {
205    pub const fn lift(&'static self) -> IsConstantKernelRef {
206        IsConstantKernelRef(ArcRef::new_ref(self))
207    }
208}
209
210impl<V: VTable + IsConstantKernel> Kernel for IsConstantKernelAdapter<V> {
211    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
212        let args = IsConstantArgs::try_from(args)?;
213        let Some(array) = args.array.as_opt::<V>() else {
214            return Ok(None);
215        };
216        let is_constant = V::is_constant(&self.0, array, args.options)?;
217        Ok(Some(Scalar::from(is_constant).into()))
218    }
219}
220
221struct IsConstantArgs<'a> {
222    array: &'a dyn Array,
223    options: &'a IsConstantOpts,
224}
225
226impl<'a> TryFrom<&InvocationArgs<'a>> for IsConstantArgs<'a> {
227    type Error = VortexError;
228
229    fn try_from(value: &InvocationArgs<'a>) -> Result<Self, Self::Error> {
230        if value.inputs.len() != 1 {
231            vortex_bail!("Expected 1 input, found {}", value.inputs.len());
232        }
233        let array = value.inputs[0]
234            .array()
235            .ok_or_else(|| vortex_err!("Expected input 0 to be an array"))?;
236        let options = value
237            .options
238            .as_any()
239            .downcast_ref::<IsConstantOpts>()
240            .ok_or_else(|| vortex_err!("Expected options to be of type IsConstantOpts"))?;
241        Ok(Self { array, options })
242    }
243}
244
245#[derive(Clone, Copy, Debug, Eq, PartialEq)]
249pub enum Cost {
250    Negligible,
252    Specialized,
256    Canonicalize,
259}
260
261#[derive(Clone, Debug)]
263pub struct IsConstantOpts {
264    pub cost: Cost,
266}
267
268impl Default for IsConstantOpts {
269    fn default() -> Self {
270        Self {
271            cost: Cost::Canonicalize,
272        }
273    }
274}
275
276impl Options for IsConstantOpts {
277    fn as_any(&self) -> &dyn Any {
278        self
279    }
280}
281
282impl IsConstantOpts {
283    pub fn is_negligible_cost(&self) -> bool {
284        self.cost == Cost::Negligible
285    }
286}