• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vortex-data / vortex / 16644538605

31 Jul 2025 08:55AM UTC coverage: 83.048% (+0.3%) from 82.767%
16644538605

Pull #4067

github

web-flow
Merge 80c73f4e7 into 9ac12db98
Pull Request #4067: chore: Enable polarsignals diffs on benchmarks

46018 of 55411 relevant lines covered (83.05%)

452793.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.57
/vortex-array/src/compute/conformance/binary_numeric.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
//! # Binary Numeric Conformance Tests
5
//!
6
//! This module provides conformance testing for binary numeric operations on Vortex arrays.
7
//! It ensures that all numeric array encodings produce identical results when performing
8
//! arithmetic operations (add, subtract, multiply, divide).
9
//!
10
//! ## Test Strategy
11
//!
12
//! For each array encoding, we test:
13
//! 1. All binary numeric operators against a constant scalar value
14
//! 2. Both left-hand and right-hand side operations (e.g., array + 1 and 1 + array)
15
//! 3. That results match the canonical primitive array implementation
16
//!
17
//! ## Supported Operations
18
//!
19
//! - Addition (`+`)
20
//! - Subtraction (`-`)
21
//! - Reverse Subtraction (scalar - array)
22
//! - Multiplication (`*`)
23
//! - Division (`/`)
24
//! - Reverse Division (scalar / array)
25

26
use itertools::Itertools;
27
use num_traits::Num;
28
use vortex_dtype::NativePType;
29
use vortex_error::{VortexExpect, VortexUnwrap, vortex_err, vortex_panic};
30
use vortex_scalar::{NumericOperator, PrimitiveScalar, Scalar};
31

32
use crate::arrays::ConstantArray;
33
use crate::compute::numeric::numeric;
34
use crate::{Array, ArrayRef, IntoArray, ToCanonical};
35

36
fn to_vec_of_scalar(array: &dyn Array) -> Vec<Scalar> {
76,832✔
37
    // Not fast, but obviously correct
38
    (0..array.len())
76,832✔
39
        .map(|index| array.scalar_at(index))
2,191,780✔
40
        .try_collect()
76,832✔
41
        .vortex_unwrap()
76,832✔
42
}
76,832✔
43

44
/// Tests binary numeric operations for conformance across array encodings.
45
///
46
/// # Type Parameters
47
///
48
/// * `T` - The native numeric type (e.g., i32, f64) that the array contains
49
///
50
/// # Arguments
51
///
52
/// * `array` - The array to test, which should contain numeric values of type `T`
53
///
54
/// # Test Details
55
///
56
/// This function:
57
/// 1. Canonicalizes the input array to primitive form to get expected values
58
/// 2. Tests all binary numeric operators against a constant value of 1
59
/// 3. Verifies results match the expected primitive array computation
60
/// 4. Tests both array-operator-scalar and scalar-operator-array forms
61
/// 5. Gracefully skips operations that would cause overflow/underflow
62
///
63
/// # Panics
64
///
65
/// Panics if:
66
/// - The array cannot be converted to primitive form
67
/// - Results don't match expected values (for operations that don't overflow)
68
fn test_binary_numeric_conformance<T: NativePType + Num + Copy>(array: ArrayRef)
1,991✔
69
where
1,991✔
70
    Scalar: From<T>,
1,991✔
71
{
72
    // First test with the standard scalar value of 1
73
    test_standard_binary_numeric::<T>(array.clone());
1,991✔
74

75
    // Then test edge cases
76
    test_binary_numeric_edge_cases(array);
1,991✔
77
}
1,991✔
78

79
fn test_standard_binary_numeric<T: NativePType + Num + Copy>(array: ArrayRef)
1,991✔
80
where
1,991✔
81
    Scalar: From<T>,
1,991✔
82
{
83
    let canonicalized_array = array
1,991✔
84
        .to_primitive()
1,991✔
85
        .vortex_expect("Failed to canonicalize array to primitive form for binary numeric test");
1,991✔
86
    let original_values = to_vec_of_scalar(&canonicalized_array.into_array());
1,991✔
87

88
    let one = T::from(1)
1,991✔
89
        .ok_or_else(|| vortex_err!("could not convert 1 into array native type"))
1,991✔
90
        .vortex_unwrap();
1,991✔
91
    let scalar_one = Scalar::from(one).cast(array.dtype()).vortex_unwrap();
1,991✔
92

93
    let operators: [NumericOperator; 6] = [
1,991✔
94
        NumericOperator::Add,
1,991✔
95
        NumericOperator::Sub,
1,991✔
96
        NumericOperator::RSub,
1,991✔
97
        NumericOperator::Mul,
1,991✔
98
        NumericOperator::Div,
1,991✔
99
        NumericOperator::RDiv,
1,991✔
100
    ];
1,991✔
101

102
    for operator in operators {
13,937✔
103
        // Test array operator scalar (e.g., array + 1)
104
        let result = numeric(
11,946✔
105
            &array,
11,946✔
106
            &ConstantArray::new(scalar_one.clone(), array.len()).into_array(),
11,946✔
107
            operator,
11,946✔
108
        );
109

110
        // Skip this operator if the entire operation fails
111
        // This can happen for some edge cases in specific encodings
112
        let Ok(result) = result else {
11,946✔
113
            continue;
839✔
114
        };
115

116
        let actual_values = to_vec_of_scalar(&result);
11,107✔
117

118
        // Check each element for overflow/underflow
119
        let expected_results: Vec<Option<Scalar>> = original_values
11,107✔
120
            .iter()
11,107✔
121
            .map(|x| {
283,793✔
122
                x.as_primitive()
283,793✔
123
                    .checked_binary_numeric(&scalar_one.as_primitive(), operator)
283,793✔
124
                    .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
283,793✔
125
            })
283,793✔
126
            .collect();
11,107✔
127

128
        // For elements that didn't overflow, check they match
129
        for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate() {
283,793✔
130
            if let Some(expected_value) = expected {
283,793✔
131
                assert_eq!(
283,793✔
132
                    actual,
133
                    expected_value,
134
                    "Binary numeric operation failed for encoding {} at index {}: \
×
135
                     ({array:?})[{idx}] {operator:?} {scalar_one} \
×
136
                     expected {expected_value:?}, got {actual:?}",
×
137
                    array.encoding_id(),
×
138
                    idx,
139
                );
140
            }
×
141
        }
142

143
        // Test scalar operator array (e.g., 1 + array)
144
        let result = numeric(
11,107✔
145
            &ConstantArray::new(scalar_one.clone(), array.len()).into_array(),
11,107✔
146
            &array,
11,107✔
147
            operator,
11,107✔
148
        );
149

150
        // Skip this operator if the entire operation fails
151
        let Ok(result) = result else {
11,107✔
152
            continue;
763✔
153
        };
154

155
        let actual_values = to_vec_of_scalar(&result);
10,344✔
156

157
        // Check each element for overflow/underflow
158
        let expected_results: Vec<Option<Scalar>> = original_values
10,344✔
159
            .iter()
10,344✔
160
            .map(|x| {
260,348✔
161
                scalar_one
260,348✔
162
                    .as_primitive()
260,348✔
163
                    .checked_binary_numeric(&x.as_primitive(), operator)
260,348✔
164
                    .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
260,348✔
165
            })
260,348✔
166
            .collect();
10,344✔
167

168
        // For elements that didn't overflow, check they match
169
        for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate() {
260,348✔
170
            if let Some(expected_value) = expected {
260,348✔
171
                assert_eq!(
260,348✔
172
                    actual,
173
                    expected_value,
174
                    "Binary numeric operation failed for encoding {} at index {}: \
×
175
                     {scalar_one} {operator:?} ({array:?})[{idx}] \
×
176
                     expected {expected_value:?}, got {actual:?}",
×
177
                    array.encoding_id(),
×
178
                    idx,
179
                );
180
            }
×
181
        }
182
    }
183
}
1,991✔
184

185
/// Entry point for binary numeric conformance testing for any array type.
186
///
187
/// This function automatically detects the array's numeric type and runs
188
/// the appropriate tests. It's designed to be called from rstest parameterized
189
/// tests without requiring explicit type parameters.
190
///
191
/// # Example
192
///
193
/// ```ignore
194
/// #[rstest]
195
/// #[case::i32_array(create_i32_array())]
196
/// #[case::f64_array(create_f64_array())]
197
/// fn test_my_encoding_binary_numeric(#[case] array: MyArray) {
198
///     test_binary_numeric_array(array.into_array());
199
/// }
200
/// ```
201
pub fn test_binary_numeric_array(array: ArrayRef) {
1,991✔
202
    use vortex_dtype::PType;
203

204
    match array.dtype() {
1,991✔
205
        vortex_dtype::DType::Primitive(ptype, _) => match ptype {
1,991✔
206
            PType::I8 => test_binary_numeric_conformance::<i8>(array),
38✔
207
            PType::I16 => test_binary_numeric_conformance::<i16>(array),
38✔
208
            PType::I32 => test_binary_numeric_conformance::<i32>(array),
539✔
209
            PType::I64 => test_binary_numeric_conformance::<i64>(array),
193✔
210
            PType::U8 => test_binary_numeric_conformance::<u8>(array),
76✔
211
            PType::U16 => test_binary_numeric_conformance::<u16>(array),
76✔
212
            PType::U32 => test_binary_numeric_conformance::<u32>(array),
305✔
213
            PType::U64 => test_binary_numeric_conformance::<u64>(array),
190✔
214
            PType::F16 => {
×
215
                // F16 not supported in num-traits, skip
×
216
                eprintln!("Skipping f16 binary numeric tests (not supported)");
×
217
            }
×
218
            PType::F32 => test_binary_numeric_conformance::<f32>(array),
268✔
219
            PType::F64 => test_binary_numeric_conformance::<f64>(array),
268✔
220
        },
221
        _ => {
222
            vortex_panic!(
×
223
                "Binary numeric tests are only supported for primitive numeric types, got {:?}",
×
224
                array.dtype()
×
225
            );
226
        }
227
    }
228
}
1,991✔
229

230
/// Tests binary numeric operations with edge case scalar values.
231
///
232
/// This function tests operations with scalar values:
233
/// - Zero (identity for addition/subtraction, absorbing for multiplication)
234
/// - Negative one (tests signed arithmetic)
235
/// - Maximum value (tests overflow behavior)
236
/// - Minimum value (tests underflow behavior)
237
fn test_binary_numeric_edge_cases(array: ArrayRef) {
1,991✔
238
    use vortex_dtype::PType;
239

240
    match array.dtype() {
1,991✔
241
        vortex_dtype::DType::Primitive(ptype, _) => match ptype {
1,991✔
242
            PType::I8 => test_binary_numeric_edge_cases_signed::<i8>(array),
38✔
243
            PType::I16 => test_binary_numeric_edge_cases_signed::<i16>(array),
38✔
244
            PType::I32 => test_binary_numeric_edge_cases_signed::<i32>(array),
539✔
245
            PType::I64 => test_binary_numeric_edge_cases_signed::<i64>(array),
193✔
246
            PType::U8 => test_binary_numeric_edge_cases_unsigned::<u8>(array),
76✔
247
            PType::U16 => test_binary_numeric_edge_cases_unsigned::<u16>(array),
76✔
248
            PType::U32 => test_binary_numeric_edge_cases_unsigned::<u32>(array),
305✔
249
            PType::U64 => test_binary_numeric_edge_cases_unsigned::<u64>(array),
190✔
250
            PType::F16 => {
×
251
                eprintln!("Skipping f16 edge case tests (not supported)");
×
252
            }
×
253
            PType::F32 => test_binary_numeric_edge_cases_float::<f32>(array),
268✔
254
            PType::F64 => test_binary_numeric_edge_cases_float::<f64>(array),
268✔
255
        },
256
        _ => {
257
            vortex_panic!(
×
258
                "Binary numeric edge case tests are only supported for primitive numeric types"
×
259
            );
260
        }
261
    }
262
}
1,991✔
263

264
fn test_binary_numeric_edge_cases_signed<T>(array: ArrayRef)
808✔
265
where
808✔
266
    T: NativePType + Num + Copy + std::fmt::Debug + num_traits::Bounded + num_traits::Signed,
808✔
267
    Scalar: From<T>,
808✔
268
{
269
    // Test with zero
270
    test_binary_numeric_with_scalar(array.clone(), T::zero());
808✔
271

272
    // Test with -1
273
    test_binary_numeric_with_scalar(array.clone(), -T::one());
808✔
274

275
    // Test with max value
276
    test_binary_numeric_with_scalar(array.clone(), T::max_value());
808✔
277

278
    // Test with min value
279
    test_binary_numeric_with_scalar(array, T::min_value());
808✔
280
}
808✔
281

282
fn test_binary_numeric_edge_cases_unsigned<T>(array: ArrayRef)
647✔
283
where
647✔
284
    T: NativePType + Num + Copy + std::fmt::Debug + num_traits::Bounded,
647✔
285
    Scalar: From<T>,
647✔
286
{
287
    // Test with zero
288
    test_binary_numeric_with_scalar(array.clone(), T::zero());
647✔
289

290
    // Test with max value
291
    test_binary_numeric_with_scalar(array, T::max_value());
647✔
292
}
647✔
293

294
fn test_binary_numeric_edge_cases_float<T>(array: ArrayRef)
536✔
295
where
536✔
296
    T: NativePType + Num + Copy + std::fmt::Debug + num_traits::Float,
536✔
297
    Scalar: From<T>,
536✔
298
{
299
    // Test with zero
300
    test_binary_numeric_with_scalar(array.clone(), T::zero());
536✔
301

302
    // Test with -1
303
    test_binary_numeric_with_scalar(array.clone(), -T::one());
536✔
304

305
    // Test with max value
306
    test_binary_numeric_with_scalar(array.clone(), T::max_value());
536✔
307

308
    // Test with min value
309
    test_binary_numeric_with_scalar(array.clone(), T::min_value());
536✔
310

311
    // Test with small positive value
312
    test_binary_numeric_with_scalar(array.clone(), T::epsilon());
536✔
313

314
    // Test with min positive value (subnormal)
315
    test_binary_numeric_with_scalar(array.clone(), T::min_positive_value());
536✔
316

317
    // Test with special float values (NaN, Infinity)
318
    test_binary_numeric_with_scalar(array.clone(), T::nan());
536✔
319
    test_binary_numeric_with_scalar(array.clone(), T::infinity());
536✔
320
    test_binary_numeric_with_scalar(array, T::neg_infinity());
536✔
321
}
536✔
322

323
fn test_binary_numeric_with_scalar<T>(array: ArrayRef, scalar_value: T)
9,350✔
324
where
9,350✔
325
    T: NativePType + Num + Copy + std::fmt::Debug,
9,350✔
326
    Scalar: From<T>,
9,350✔
327
{
328
    let canonicalized_array = array
9,350✔
329
        .to_primitive()
9,350✔
330
        .vortex_expect("Failed to canonicalize array to primitive form for binary numeric test");
9,350✔
331
    let original_values = to_vec_of_scalar(&canonicalized_array.into_array());
9,350✔
332

333
    let scalar = Scalar::from(scalar_value)
9,350✔
334
        .cast(array.dtype())
9,350✔
335
        .vortex_unwrap();
9,350✔
336

337
    // Only test operators that make sense for the given scalar
338
    let operators = if scalar_value == T::zero() {
9,350✔
339
        // Skip division by zero
340
        vec![
1,991✔
341
            NumericOperator::Add,
1,991✔
342
            NumericOperator::Sub,
1,991✔
343
            NumericOperator::RSub,
1,991✔
344
            NumericOperator::Mul,
1,991✔
345
        ]
346
    } else {
347
        vec![
7,359✔
348
            NumericOperator::Add,
7,359✔
349
            NumericOperator::Sub,
7,359✔
350
            NumericOperator::RSub,
7,359✔
351
            NumericOperator::Mul,
7,359✔
352
            NumericOperator::Div,
7,359✔
353
            NumericOperator::RDiv,
7,359✔
354
        ]
355
    };
356

357
    for operator in operators {
61,468✔
358
        // Test array operator scalar
359
        let result = numeric(
52,118✔
360
            &array,
52,118✔
361
            &ConstantArray::new(scalar.clone(), array.len()).into_array(),
52,118✔
362
            operator,
52,118✔
363
        );
364

365
        // Skip if the entire operation fails
366
        if result.is_err() {
52,118✔
367
            continue;
8,078✔
368
        }
44,040✔
369

370
        let result = result.vortex_unwrap();
44,040✔
371
        let actual_values = to_vec_of_scalar(&result);
44,040✔
372

373
        // Check each element for overflow/underflow
374
        let expected_results: Vec<Option<Scalar>> = original_values
44,040✔
375
            .iter()
44,040✔
376
            .map(|x| {
1,319,444✔
377
                x.as_primitive()
1,319,444✔
378
                    .checked_binary_numeric(&scalar.as_primitive(), operator)
1,319,444✔
379
                    .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
1,319,444✔
380
            })
1,319,444✔
381
            .collect();
44,040✔
382

383
        // For elements that didn't overflow, check they match
384
        for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate() {
1,319,444✔
385
            if let Some(expected_value) = expected {
1,319,444✔
386
                assert_eq!(
1,319,444✔
387
                    actual,
388
                    expected_value,
389
                    "Binary numeric operation failed for encoding {} at index {} with scalar {:?}: \
×
390
                     ({array:?})[{idx}] {operator:?} {scalar} \
×
391
                     expected {expected_value:?}, got {actual:?}",
×
392
                    array.encoding_id(),
×
393
                    idx,
394
                    scalar_value,
395
                );
396
            }
×
397
        }
398
    }
399
}
9,350✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc