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 {} canonicalize returned None",
81 array
82 );
83 }
84
85 if let Some(value) = value {
87 array
88 .statistics()
89 .set(Stat::IsConstant, Precision::Exact(value.into()));
90 }
91
92 Ok(Scalar::from(value).into())
93 }
94
95 fn return_dtype(&self, _args: &InvocationArgs) -> VortexResult<DType> {
96 Ok(DType::Bool(Nullability::Nullable))
99 }
100
101 fn return_len(&self, _args: &InvocationArgs) -> VortexResult<usize> {
102 Ok(1)
103 }
104
105 fn is_elementwise(&self) -> bool {
106 false
107 }
108}
109
110fn is_constant_impl(
111 array: &dyn Array,
112 options: &IsConstantOpts,
113 kernels: &[ArcRef<dyn Kernel>],
114) -> VortexResult<Option<bool>> {
115 match array.len() {
116 0 => return Ok(Some(false)),
118 1 => return Ok(Some(true)),
120 _ => {}
121 }
122
123 if array.as_opt::<ConstantVTable>().is_some() || array.as_opt::<NullVTable>().is_some() {
125 return Ok(Some(true));
126 }
127
128 let all_invalid = array.all_invalid()?;
129 if all_invalid {
130 return Ok(Some(true));
131 }
132
133 let all_valid = array.all_valid()?;
134
135 if !all_valid && !all_invalid {
137 return Ok(Some(false));
138 }
139
140 let min = array
142 .statistics()
143 .get_scalar(Stat::Min, array.dtype())
144 .and_then(|p| p.as_exact());
145 let max = array
146 .statistics()
147 .get_scalar(Stat::Max, array.dtype())
148 .and_then(|p| p.as_exact());
149
150 if let Some((min, max)) = min.zip(max) {
151 if min == max {
152 return Ok(Some(true));
153 }
154 }
155
156 assert!(
157 all_valid,
158 "All values must be valid as an invariant of the VTable."
159 );
160 let args = InvocationArgs {
161 inputs: &[array.into()],
162 options,
163 };
164 for kernel in kernels {
165 if let Some(output) = kernel.invoke(&args)? {
166 return Ok(output.unwrap_scalar()?.as_bool().value());
167 }
168 }
169 if let Some(output) = array.invoke(&IS_CONSTANT_FN, &args)? {
170 return Ok(output.unwrap_scalar()?.as_bool().value());
171 }
172
173 log::debug!(
174 "No is_constant implementation found for {}",
175 array.encoding_id()
176 );
177
178 if options.cost == Cost::Canonicalize && !array.is_canonical() {
179 let array = array.to_canonical()?;
180 let is_constant = is_constant_opts(array.as_ref(), options)?;
181 return Ok(is_constant);
182 }
183
184 Ok(None)
186}
187
188pub struct IsConstantKernelRef(ArcRef<dyn Kernel>);
189inventory::collect!(IsConstantKernelRef);
190
191pub trait IsConstantKernel: VTable {
192 fn is_constant(&self, array: &Self::Array, opts: &IsConstantOpts)
199 -> VortexResult<Option<bool>>;
200}
201
202#[derive(Debug)]
203pub struct IsConstantKernelAdapter<V: VTable>(pub V);
204
205impl<V: VTable + IsConstantKernel> IsConstantKernelAdapter<V> {
206 pub const fn lift(&'static self) -> IsConstantKernelRef {
207 IsConstantKernelRef(ArcRef::new_ref(self))
208 }
209}
210
211impl<V: VTable + IsConstantKernel> Kernel for IsConstantKernelAdapter<V> {
212 fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
213 let args = IsConstantArgs::try_from(args)?;
214 let Some(array) = args.array.as_opt::<V>() else {
215 return Ok(None);
216 };
217 let is_constant = V::is_constant(&self.0, array, args.options)?;
218 Ok(Some(Scalar::from(is_constant).into()))
219 }
220}
221
222struct IsConstantArgs<'a> {
223 array: &'a dyn Array,
224 options: &'a IsConstantOpts,
225}
226
227impl<'a> TryFrom<&InvocationArgs<'a>> for IsConstantArgs<'a> {
228 type Error = VortexError;
229
230 fn try_from(value: &InvocationArgs<'a>) -> Result<Self, Self::Error> {
231 if value.inputs.len() != 1 {
232 vortex_bail!("Expected 1 input, found {}", value.inputs.len());
233 }
234 let array = value.inputs[0]
235 .array()
236 .ok_or_else(|| vortex_err!("Expected input 0 to be an array"))?;
237 let options = value
238 .options
239 .as_any()
240 .downcast_ref::<IsConstantOpts>()
241 .ok_or_else(|| vortex_err!("Expected options to be of type IsConstantOpts"))?;
242 Ok(Self { array, options })
243 }
244}
245
246#[derive(Clone, Copy, Debug, Eq, PartialEq)]
250pub enum Cost {
251 Negligible,
253 Specialized,
257 Canonicalize,
260}
261
262#[derive(Clone, Debug)]
264pub struct IsConstantOpts {
265 pub cost: Cost,
267}
268
269impl Default for IsConstantOpts {
270 fn default() -> Self {
271 Self {
272 cost: Cost::Canonicalize,
273 }
274 }
275}
276
277impl Options for IsConstantOpts {
278 fn as_any(&self) -> &dyn Any {
279 self
280 }
281}
282
283impl IsConstantOpts {
284 pub fn is_negligible_cost(&self) -> bool {
285 self.cost == Cost::Negligible
286 }
287}