Skip to main content

vortex_array/compute/conformance/
mask.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexExpect;
5use vortex_mask::Mask;
6
7use crate::ArrayRef;
8use crate::ExecutionCtx;
9use crate::IntoArray;
10use crate::RecursiveCanonical;
11use crate::arrays::BoolArray;
12use crate::arrays::bool::BoolArrayExt;
13use crate::builtins::ArrayBuiltins;
14use crate::validity::Validity;
15
16/// Test mask compute function with various array sizes and patterns.
17/// The mask operation sets elements to null where the mask is true.
18pub fn test_mask_conformance(array: &ArrayRef, ctx: &mut ExecutionCtx) {
19    let len = array.len();
20
21    if len > 0 {
22        test_heterogenous_mask(array, ctx);
23        test_empty_mask(array, ctx);
24        test_full_mask(array, ctx);
25        test_alternating_mask(array, ctx);
26        test_sparse_mask(array, ctx);
27        test_single_element_mask(array, ctx);
28    }
29
30    if len >= 5 {
31        test_double_mask(array, ctx);
32    }
33
34    if len > 0 {
35        test_nullable_mask_input(array, ctx);
36    }
37}
38
39/// Tests masking with a heterogeneous pattern
40fn test_heterogenous_mask(array: &ArrayRef, ctx: &mut ExecutionCtx) {
41    let len = array.len();
42
43    // Create a pattern where roughly half the values are masked
44    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 3 != 1).collect();
45    let mask_array = Mask::from_iter(mask_pattern.clone());
46
47    let masked = array
48        .clone()
49        .mask((!&mask_array).into_array())
50        .vortex_expect("mask should succeed in conformance test")
51        .execute::<RecursiveCanonical>(ctx)
52        .vortex_expect("mask execution should succeed in conformance test")
53        .0
54        .into_array();
55    assert_eq!(masked.len(), array.len());
56
57    // Verify masked elements are null and unmasked elements are preserved
58    for (i, &masked_out) in mask_pattern.iter().enumerate() {
59        if masked_out {
60            assert!(
61                !masked
62                    .is_valid(i, ctx)
63                    .vortex_expect("is_valid should succeed in conformance test")
64            );
65        } else {
66            assert_eq!(
67                masked
68                    .execute_scalar(i, ctx)
69                    .vortex_expect("scalar_at should succeed in conformance test"),
70                array
71                    .execute_scalar(i, ctx)
72                    .vortex_expect("scalar_at should succeed in conformance test")
73                    .into_nullable()
74            );
75        }
76    }
77}
78
79/// Tests that an empty mask (all false) preserves all elements
80pub(super) fn test_empty_mask(array: &ArrayRef, ctx: &mut ExecutionCtx) {
81    let len = array.len();
82    let all_unmasked = vec![false; len];
83    let mask_array = Mask::from_iter(all_unmasked);
84
85    let masked = array
86        .clone()
87        .mask((!&mask_array).into_array())
88        .vortex_expect("mask should succeed in conformance test")
89        .execute::<RecursiveCanonical>(ctx)
90        .vortex_expect("mask execution should succeed in conformance test")
91        .0
92        .into_array();
93    assert_eq!(masked.len(), array.len());
94    assert!(masked.dtype().is_nullable());
95
96    // All elements should be preserved
97    for i in 0..len {
98        assert_eq!(
99            masked
100                .execute_scalar(i, ctx)
101                .vortex_expect("scalar_at should succeed in conformance test"),
102            array
103                .execute_scalar(i, ctx)
104                .vortex_expect("scalar_at should succeed in conformance test")
105                .into_nullable()
106        );
107    }
108}
109
110/// Tests that a full mask (all true) makes all elements null
111fn test_full_mask(array: &ArrayRef, ctx: &mut ExecutionCtx) {
112    let len = array.len();
113    let all_masked = vec![true; len];
114    let mask_array = Mask::from_iter(all_masked);
115
116    let masked = array
117        .clone()
118        .mask((!&mask_array).into_array())
119        .vortex_expect("mask should succeed in conformance test")
120        .execute::<RecursiveCanonical>(ctx)
121        .vortex_expect("mask execution should succeed in conformance test")
122        .0
123        .into_array();
124    assert_eq!(masked.len(), array.len());
125
126    // All elements should be null
127    for i in 0..len {
128        assert!(
129            !masked
130                .is_valid(i, ctx)
131                .vortex_expect("is_valid should succeed in conformance test")
132        );
133    }
134}
135
136/// Tests alternating mask pattern
137fn test_alternating_mask(array: &ArrayRef, ctx: &mut ExecutionCtx) {
138    let len = array.len();
139    let pattern: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
140    let mask_array = Mask::from_iter(pattern);
141
142    let masked = array
143        .clone()
144        .mask((!&mask_array).into_array())
145        .vortex_expect("mask should succeed in conformance test")
146        .execute::<RecursiveCanonical>(ctx)
147        .vortex_expect("mask execution should succeed in conformance test")
148        .0
149        .into_array();
150    assert_eq!(masked.len(), array.len());
151
152    for i in 0..len {
153        if i % 2 == 0 {
154            assert!(
155                !masked
156                    .is_valid(i, ctx)
157                    .vortex_expect("is_valid should succeed in conformance test")
158            );
159        } else {
160            assert_eq!(
161                masked
162                    .execute_scalar(i, ctx)
163                    .vortex_expect("scalar_at should succeed in conformance test"),
164                array
165                    .execute_scalar(i, ctx)
166                    .vortex_expect("scalar_at should succeed in conformance test")
167                    .into_nullable()
168            );
169        }
170    }
171}
172
173/// Tests sparse mask (only a few elements masked)
174fn test_sparse_mask(array: &ArrayRef, ctx: &mut ExecutionCtx) {
175    let len = array.len();
176    if len < 10 {
177        return; // Skip for small arrays
178    }
179
180    // Mask only about 10% of elements
181    let pattern: Vec<bool> = (0..len).map(|i| i % 10 == 0).collect();
182    let mask_array = Mask::from_iter(pattern.clone());
183
184    let masked = array
185        .clone()
186        .mask((!&mask_array).into_array())
187        .vortex_expect("mask should succeed in conformance test")
188        .execute::<RecursiveCanonical>(ctx)
189        .vortex_expect("mask execution should succeed in conformance test")
190        .0
191        .into_array();
192    assert_eq!(masked.len(), array.len());
193
194    for (i, &masked_out) in pattern.iter().enumerate() {
195        let actual = masked
196            .execute_scalar(i, ctx)
197            .vortex_expect("scalar_at should succeed in conformance test");
198        if masked_out {
199            assert!(actual.is_null(), "masked index {i} should be null");
200        } else {
201            assert_eq!(
202                actual,
203                array
204                    .execute_scalar(i, ctx)
205                    .vortex_expect("scalar_at should succeed in conformance test")
206                    .into_nullable(),
207                "unmasked index {i} should preserve its value"
208            );
209        }
210    }
211}
212
213/// Tests masking a single element
214fn test_single_element_mask(array: &ArrayRef, ctx: &mut ExecutionCtx) {
215    let len = array.len();
216
217    // Mask only the first element
218    let mut pattern = vec![false; len];
219    pattern[0] = true;
220    let mask_array = Mask::from_iter(pattern);
221
222    let masked = array
223        .clone()
224        .mask((!&mask_array).into_array())
225        .vortex_expect("mask should succeed in conformance test")
226        .execute::<RecursiveCanonical>(ctx)
227        .vortex_expect("mask execution should succeed in conformance test")
228        .0
229        .into_array();
230    assert!(
231        !masked
232            .is_valid(0, ctx)
233            .vortex_expect("is_valid should succeed in conformance test")
234    );
235
236    for i in 1..len {
237        assert_eq!(
238            masked
239                .execute_scalar(i, ctx)
240                .vortex_expect("scalar_at should succeed in conformance test"),
241            array
242                .execute_scalar(i, ctx)
243                .vortex_expect("scalar_at should succeed in conformance test")
244                .into_nullable()
245        );
246    }
247}
248
249/// Tests double masking operations
250fn test_double_mask(array: &ArrayRef, ctx: &mut ExecutionCtx) {
251    let len = array.len();
252
253    // Create two different mask patterns
254    let mask1_pattern: Vec<bool> = (0..len).map(|i| i % 3 == 0).collect();
255    let mask2_pattern: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
256
257    let mask1 = Mask::from_iter(mask1_pattern.clone());
258    let mask2 = Mask::from_iter(mask2_pattern.clone());
259
260    let first_masked = array
261        .clone()
262        .mask((!&mask1).into_array())
263        .vortex_expect("mask should succeed in conformance test");
264    let double_masked = first_masked
265        .mask((!&mask2).into_array())
266        .vortex_expect("mask should succeed in conformance test")
267        .execute::<RecursiveCanonical>(ctx)
268        .vortex_expect("mask execution should succeed in conformance test")
269        .0
270        .into_array();
271
272    // Elements should be null if either mask is true
273    for i in 0..len {
274        if mask1_pattern[i] || mask2_pattern[i] {
275            assert!(
276                !double_masked
277                    .is_valid(i, ctx)
278                    .vortex_expect("is_valid should succeed in conformance test")
279            );
280        } else {
281            assert_eq!(
282                double_masked
283                    .execute_scalar(i, ctx)
284                    .vortex_expect("scalar_at should succeed in conformance test"),
285                array
286                    .execute_scalar(i, ctx)
287                    .vortex_expect("scalar_at should succeed in conformance test")
288                    .into_nullable()
289            );
290        }
291    }
292}
293
294/// Tests masking with nullable mask (nulls treated as false)
295fn test_nullable_mask_input(array: &ArrayRef, ctx: &mut ExecutionCtx) {
296    let len = array.len();
297    if len < 3 {
298        return; // Skip for very small arrays
299    }
300
301    // Create a nullable mask
302    let bool_values: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
303    let validity_values: Vec<bool> = (0..len).map(|i| i % 3 != 0).collect();
304
305    let bool_array = BoolArray::from_iter(bool_values.clone());
306    let validity = Validity::from_iter(validity_values.clone());
307    let nullable_mask = BoolArray::new(bool_array.to_bit_buffer(), validity);
308
309    let mask_array = nullable_mask.to_mask_fill_null_false(ctx);
310    let masked = array
311        .clone()
312        .mask((!&mask_array).into_array())
313        .vortex_expect("mask should succeed in conformance test")
314        .execute::<RecursiveCanonical>(ctx)
315        .vortex_expect("mask execution should succeed in conformance test")
316        .0
317        .into_array();
318
319    // Elements are masked only if the mask is true AND valid
320    for i in 0..len {
321        if bool_values[i] && validity_values[i] {
322            assert!(
323                !masked
324                    .is_valid(i, ctx)
325                    .vortex_expect("is_valid should succeed in conformance test")
326            );
327        } else {
328            assert_eq!(
329                masked
330                    .execute_scalar(i, ctx)
331                    .vortex_expect("scalar_at should succeed in conformance test"),
332                array
333                    .execute_scalar(i, ctx)
334                    .vortex_expect("scalar_at should succeed in conformance test")
335                    .into_nullable()
336            );
337        }
338    }
339}