Skip to content

Commit a83f272

Browse files
authored
fix: update sequential_add_mul to account for precision errors (#80)
* fix: update sequential_add_mul (allow for minor precision error) * ci: ubuntu22 for python 3.7 * fix: use epsilon in sequential_add_mul to avoid precision errors
1 parent cd68c99 commit a83f272

File tree

5 files changed

+70
-13
lines changed

5 files changed

+70
-13
lines changed

.github/workflows/ci-tsdownsample.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,13 @@ jobs:
5050
exclude: # Python < 3.8 is not supported on Apple Silicon ARM64
5151
- os: macOS-latest
5252
python-version: '3.7'
53+
- os: ubuntu-latest
54+
python-version: '3.7'
5355
include: # So run on older version on Intel CPU
5456
- os: macOS-13
5557
python-version: '3.7'
58+
- os: ubuntu-22.04
59+
python-version: '3.7'
5660

5761
env:
5862
PYTHON: ${{ matrix.python-version }}

downsample_rs/src/m4.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ mod tests {
447447
let idxs3 = m4_without_x_parallel(arr.as_slice(), n_out);
448448
let idxs4 = m4_with_x_parallel(&x, arr.as_slice(), n_out);
449449
assert_eq!(idxs1, idxs3);
450+
// TODO: check whether this still fails after fixing the sequential_add_mul
450451
assert_eq!(idxs1, idxs4); // TODO: this fails when nb. of threads = 16
451452
}
452453
}

downsample_rs/src/minmax.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,17 @@ mod tests {
415415
assert_eq!(sampled_indices, expected_indices);
416416
}
417417

418+
#[test]
419+
fn test_same_output() {
420+
const N: usize = 1001 - 2;
421+
const n_out: usize = 26 * 4;
422+
let y = (0..N).map(|v| v as f32).collect::<Vec<f32>>();
423+
let x = (1..(N + 1) as i32).collect::<Vec<i32>>();
424+
let sampled_indices1 = min_max_with_x(&x, &y, n_out);
425+
let sampled_indices2 = min_max_without_x(&y, n_out);
426+
assert_eq!(sampled_indices1, sampled_indices2);
427+
}
428+
418429
#[apply(n_outs)]
419430
fn test_many_random_runs_same_output(n_out: usize) {
420431
const N: usize = 20_003;

downsample_rs/src/minmaxlttb.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,25 @@ mod tests {
258258
assert_eq!(sampled_indices, vec![0, 1, 5, 9]);
259259
}
260260

261+
#[test]
262+
fn test_same_output() {
263+
let N: usize = 2001;
264+
let n_out: usize = 100;
265+
let y = (0..N).map(|v| v as f32).collect::<Vec<f32>>();
266+
let x = (0..N as i32).collect::<Vec<i32>>();
267+
let sampled_indices1 = minmaxlttb_with_x(&x, &y, n_out, 4);
268+
let sampled_indices2 = minmaxlttb_without_x(&y, n_out, 4);
269+
assert_eq!(sampled_indices1, sampled_indices2);
270+
271+
let N: usize = 1001;
272+
let n_out: usize = 26;
273+
let y = (0..N).map(|v| v as f32).collect::<Vec<f32>>();
274+
let x = (0..N as i32).collect::<Vec<i32>>();
275+
let sampled_indices1 = minmaxlttb_with_x(&x, &y, n_out, 4);
276+
let sampled_indices2 = minmaxlttb_without_x(&y, n_out, 4);
277+
assert_eq!(sampled_indices1, sampled_indices2);
278+
}
279+
261280
#[apply(n_outs)]
262281
fn test_many_random_runs_same_output(n_out: usize) {
263282
const N: usize = 20_000;

downsample_rs/src/searchsorted.rs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use super::types::Num;
55
use super::POOL;
66
use num_traits::{AsPrimitive, FromPrimitive};
77

8+
const EPSILON: f64 = 1e-12; // Small value to avoid precision errors
9+
810
// ---------------------- Binary search ----------------------
911

1012
/// Binary search for the index position of the given value in the given array.
@@ -74,6 +76,17 @@ fn binary_search_with_mid<T: Copy + PartialOrd>(
7476

7577
// ------------------- Equidistant binning --------------------
7678

79+
#[inline(always)]
80+
fn sequential_add_mul(start_val: f64, add_val: f64, mul: usize, epsilon: f64) -> f64 {
81+
// start_val + add_val * mul will sometimes overflow when add_val * mul is
82+
// larger than the largest positive f64 number.
83+
// This code should not fail when: (f64::MAX - start_val) < (add_val * mul).
84+
// -> Note that f64::MAX - start_val can be up to 2 * f64::MAX.
85+
let mul_2: f64 = mul as f64 / 2.0;
86+
// start_val + add_val * mul_2 as f64 + add_val * (mul - mul_2) as f64
87+
start_val + add_val * mul_2 + add_val * mul_2 + epsilon
88+
}
89+
7790
// --- Sequential version
7891

7992
pub(crate) fn get_equidistant_bin_idx_iterator<T>(
@@ -100,7 +113,8 @@ where
100113
let start_idx: usize = idx; // Start index of the bin (previous end index)
101114

102115
// Update the search value
103-
let search_value: T = T::from_f64(arr0 + val_step * (i + 1) as f64).unwrap();
116+
let search_value: T =
117+
T::from_f64(sequential_add_mul(arr0, val_step, i + 1, EPSILON)).unwrap();
104118
if arr[start_idx] >= search_value {
105119
// If the first value of the bin is already >= the search value,
106120
// then the bin is empty.
@@ -116,16 +130,6 @@ where
116130

117131
// --- Parallel version
118132

119-
#[inline(always)]
120-
fn sequential_add_mul(start_val: f64, add_val: f64, mul: usize) -> f64 {
121-
// start_val + add_val * mul will sometimes overflow when add_val * mul is
122-
// larger than the largest positive f64 number.
123-
// This code should not fail when: (f64::MAX - start_val) < (add_val * mul).
124-
// -> Note that f64::MAX - start_val can be up to 2 * f64::MAX.
125-
let mul_2: usize = mul / 2;
126-
start_val + add_val * mul_2 as f64 + add_val * (mul - mul_2) as f64
127-
}
128-
129133
pub(crate) fn get_equidistant_bin_idx_iterator_parallel<T>(
130134
arr: &[T],
131135
nb_bins: usize,
@@ -139,16 +143,18 @@ where
139143
let val_step: f64 =
140144
(arr[arr.len() - 1].as_() / nb_bins as f64) - (arr[0].as_() / nb_bins as f64);
141145
let arr0: f64 = arr[0].as_(); // The first value of the array
142-
// 2. Compute the number of threads & bins per thread
146+
147+
// 2. Compute the number of threads & bins per thread
143148
let n_threads = std::cmp::min(POOL.current_num_threads(), nb_bins);
144149
let nb_bins_per_thread = nb_bins / n_threads;
145150
let nb_bins_last_thread = nb_bins - nb_bins_per_thread * (n_threads - 1);
151+
146152
// 3. Iterate over the number of threads
147153
// -> for each thread perform the binary search sorted with moving left and
148154
// yield the indices (using the same idea as for the sequential version)
149155
(0..n_threads).into_par_iter().map(move |i| {
150156
// The moving index & value (for the thread)
151-
let arr0_thr: f64 = sequential_add_mul(arr0, val_step, i * nb_bins_per_thread); // Search value
157+
let arr0_thr: f64 = sequential_add_mul(arr0, val_step, i * nb_bins_per_thread, EPSILON); // Search value
152158
let start_value: T = T::from_f64(arr0_thr).unwrap();
153159
// Search the start of the fist bin (of the thread)
154160
let mut idx: usize = 0; // Index of the search value
@@ -198,6 +204,22 @@ mod tests {
198204
#[case(101)]
199205
fn nb_bins(#[case] nb_bins: usize) {}
200206

207+
#[test]
208+
fn test_sequential_add_mul() {
209+
assert_eq!(sequential_add_mul(0.0, 1.0, 0, 0.0), 0.0);
210+
assert_eq!(sequential_add_mul(-1.0, 1.0, 1, 0.0), 0.0);
211+
assert_eq!(sequential_add_mul(-1.0, 1.0, 1, EPSILON), EPSILON);
212+
// Really large values
213+
assert_eq!(sequential_add_mul(0.0, 1.0, 1_000_000, 0.0), 1_000_000.0);
214+
assert!(sequential_add_mul(f64::MIN, f64::MAX / 2.0, 3, 0.0) < f64::MAX,);
215+
// TODO: the next tests fails due to very minor precision error
216+
// -> however, this precision error is needed to avoid the issue with m4_with_x
217+
// assert_eq!(
218+
// sequential_add_mul(f64::MIN, f64::MAX / 2.0, 3, 0.0),
219+
// f64::MIN + f64::MAX / 2.0 + f64::MAX
220+
// );
221+
}
222+
201223
#[test]
202224
fn test_search_sorted_identicial_to_np_linspace_searchsorted() {
203225
// Create a 0..9999 array

0 commit comments

Comments
 (0)