Skip to main content

oxiphysics_gpu/parallel/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use rayon::prelude::*;
6
7use super::types::{LoadBalancePlan, LoadBalanceStrategy, WorkStealQueue};
8
9#[inline]
10pub(super) fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
11    let dx = a[0] - b[0];
12    let dy = a[1] - b[1];
13    let dz = a[2] - b[2];
14    (dx * dx + dy * dy + dz * dz).sqrt()
15}
16/// Parallel SPH density computation.
17///
18/// For each particle `i`, computes `rho_i = sum_j m_j * W(|r_i - r_j|, h)`.
19/// The outer loop runs in parallel via Rayon.
20///
21/// # Arguments
22/// * `positions`  - slice of 3-D particle positions.
23/// * `masses`     - per-particle masses (same length as `positions`).
24/// * `h`          - smoothing length.
25/// * `kernel_fn`  - smoothing kernel `W(r, h)` callable from multiple threads.
26pub fn parallel_sph_density(
27    positions: &[[f64; 3]],
28    masses: &[f64],
29    h: f64,
30    kernel_fn: impl Fn(f64, f64) -> f64 + Sync,
31) -> Vec<f64> {
32    positions
33        .par_iter()
34        .map(|&pi| {
35            positions
36                .iter()
37                .zip(masses.iter())
38                .map(|(&pj, &mj)| mj * kernel_fn(dist3(pi, pj), h))
39                .sum()
40        })
41        .collect()
42}
43/// Parallel pairwise force accumulation.
44///
45/// Computes the net force on every particle.  The outer loop (over particle
46/// `i`) runs in parallel; each thread independently sums the contributions
47/// from all `j != i`.
48///
49/// # Arguments
50/// * `positions` - particle positions.
51/// * `n`         - number of particles (must equal `positions.len()`).
52/// * `force_fn`  - `force_fn(i, j, r_ij) -> force_on_i_from_j`.
53pub fn parallel_pairwise_forces(
54    positions: &[[f64; 3]],
55    n: usize,
56    force_fn: impl Fn(usize, usize, [f64; 3]) -> [f64; 3] + Sync,
57) -> Vec<[f64; 3]> {
58    (0..n)
59        .into_par_iter()
60        .map(|i| {
61            let mut f = [0.0f64; 3];
62            for j in 0..n {
63                if i == j {
64                    continue;
65                }
66                let r_ij = [
67                    positions[j][0] - positions[i][0],
68                    positions[j][1] - positions[i][1],
69                    positions[j][2] - positions[i][2],
70                ];
71                let fij = force_fn(i, j, r_ij);
72                f[0] += fij[0];
73                f[1] += fij[1];
74                f[2] += fij[2];
75            }
76            f
77        })
78        .collect()
79}
80/// Parallel Lennard-Jones (12-6) force computation.
81///
82/// For each particle `i`, accumulates contributions from all `j` within
83/// `cutoff`.  The potential is `U = 4*eps*[(sig/r)^12 - (sig/r)^6]`, giving
84/// `F_i = sum_{j!=i} 4*eps [12(sig/r)^12 - 6(sig/r)^6] / r * r_hat_ij`.
85///
86/// Interactions beyond `cutoff` are skipped.
87pub fn parallel_lj_forces(
88    positions: &[[f64; 3]],
89    epsilon: f64,
90    sigma: f64,
91    cutoff: f64,
92) -> Vec<[f64; 3]> {
93    let n = positions.len();
94    (0..n)
95        .into_par_iter()
96        .map(|i| {
97            let mut f = [0.0f64; 3];
98            for j in 0..n {
99                if i == j {
100                    continue;
101                }
102                let dx = positions[j][0] - positions[i][0];
103                let dy = positions[j][1] - positions[i][1];
104                let dz = positions[j][2] - positions[i][2];
105                let r2 = dx * dx + dy * dy + dz * dz;
106                let r = r2.sqrt();
107                if r >= cutoff || r < 1e-12 {
108                    continue;
109                }
110                let sr = sigma / r;
111                let sr6 = sr.powi(6);
112                let sr12 = sr6 * sr6;
113                let mag = 4.0 * epsilon * (12.0 * sr12 - 6.0 * sr6) / r2;
114                f[0] -= mag * dx;
115                f[1] -= mag * dy;
116                f[2] -= mag * dz;
117            }
118            f
119        })
120        .collect()
121}
122/// Parallel velocity-Verlet position and velocity half-update.
123///
124/// Updates positions with `x += v*dt + 0.5*a*dt^2` and velocities with
125/// `v += 0.5*a*dt` (first half of the Verlet velocity update; call again
126/// after recomputing forces for the second half).
127///
128/// The loop runs in parallel via `par_iter_mut`.
129pub fn parallel_verlet_step(
130    positions: &mut Vec<[f64; 3]>,
131    velocities: &mut Vec<[f64; 3]>,
132    forces: &[[f64; 3]],
133    masses: &[f64],
134    dt: f64,
135) {
136    positions
137        .par_iter_mut()
138        .zip(velocities.par_iter_mut())
139        .zip(forces.par_iter())
140        .zip(masses.par_iter())
141        .for_each(|(((pos, vel), force), &mass)| {
142            let inv_m = 1.0 / mass;
143            for k in 0..3 {
144                let a = force[k] * inv_m;
145                pos[k] += vel[k] * dt + 0.5 * a * dt * dt;
146                vel[k] += 0.5 * a * dt;
147            }
148        });
149}
150/// Parallel AABB overlap detection.
151///
152/// Returns all pairs `(i, j)` with `i < j` whose axis-aligned bounding boxes
153/// overlap.  The outer loop runs in parallel; each thread contributes matching
154/// pairs into a local vector that is then flattened.
155pub fn parallel_aabb_pairs(aabbs: &[([f64; 3], [f64; 3])]) -> Vec<(usize, usize)> {
156    let n = aabbs.len();
157    (0..n)
158        .into_par_iter()
159        .flat_map(|i| {
160            let mut local = Vec::new();
161            let (min_i, max_i) = aabbs[i];
162            for (j, &(min_j, max_j)) in aabbs.iter().enumerate().skip(i + 1) {
163                let overlap = (0..3).all(|k| min_i[k] <= max_j[k] && min_j[k] <= max_i[k]);
164                if overlap {
165                    local.push((i, j));
166                }
167            }
168            local
169        })
170        .collect()
171}
172/// Execute `f(i)` for `i` in `0..n`, splitting into chunks of `chunk_size`.
173///
174/// Currently processes chunks sequentially; prefer [`parallel_sph_density`]
175/// and the other parallel kernels for performance-critical code.
176pub fn parallel_for(n: usize, chunk_size: usize, f: impl Fn(usize)) {
177    let cs = if chunk_size == 0 { 1 } else { chunk_size };
178    for start in (0..n).step_by(cs) {
179        let end = (start + cs).min(n);
180        for i in start..end {
181            f(i);
182        }
183    }
184}
185/// Parallel sum reduction using Rayon.
186pub fn parallel_reduce_sum(data: &[f64]) -> f64 {
187    data.par_iter().copied().sum()
188}
189/// Parallel max reduction using Rayon.
190pub fn parallel_reduce_max(data: &[f64]) -> f64 {
191    data.par_iter()
192        .copied()
193        .reduce(|| f64::NEG_INFINITY, f64::max)
194}
195/// Parallel min reduction using Rayon.
196pub fn parallel_reduce_min(data: &[f64]) -> f64 {
197    data.par_iter().copied().reduce(|| f64::INFINITY, f64::min)
198}
199/// Parallel dot product of two slices.
200pub fn parallel_dot_product(a: &[f64], b: &[f64]) -> f64 {
201    a.par_iter()
202        .zip(b.par_iter())
203        .map(|(&ai, &bi)| ai * bi)
204        .sum()
205}
206/// Parallel L2 norm (Euclidean norm).
207pub fn parallel_norm2(data: &[f64]) -> f64 {
208    let sum_sq: f64 = data.par_iter().map(|&x| x * x).sum();
209    sum_sq.sqrt()
210}
211/// Parallel mean.
212pub fn parallel_mean(data: &[f64]) -> f64 {
213    if data.is_empty() {
214        return 0.0;
215    }
216    let sum: f64 = data.par_iter().copied().sum();
217    sum / data.len() as f64
218}
219/// Parallel variance (population variance).
220pub fn parallel_variance(data: &[f64]) -> f64 {
221    if data.is_empty() {
222        return 0.0;
223    }
224    let mean = parallel_mean(data);
225    let sum_sq: f64 = data.par_iter().map(|&x| (x - mean) * (x - mean)).sum();
226    sum_sq / data.len() as f64
227}
228/// Two-pass parallel reduction: compute both sum and count in one pass.
229pub fn parallel_sum_count(data: &[f64]) -> (f64, usize) {
230    data.par_iter()
231        .copied()
232        .fold(|| (0.0f64, 0usize), |(s, c), x| (s + x, c + 1))
233        .reduce(|| (0.0, 0), |(s1, c1), (s2, c2)| (s1 + s2, c1 + c2))
234}
235/// Parallel reduction with a custom binary operator.
236///
237/// `identity` is the identity element for the operator (e.g. 0.0 for add).
238/// `op` must be associative and commutative for correctness.
239pub fn parallel_reduce_custom(
240    data: &[f64],
241    identity: f64,
242    op: impl Fn(f64, f64) -> f64 + Sync + Send,
243) -> f64 {
244    data.par_iter().copied().reduce(|| identity, op)
245}
246/// Parallel exclusive prefix sum using a two-pass algorithm.
247///
248/// Phase 1: compute partial sums in chunks (parallel).
249/// Phase 2: propagate offsets (sequential).
250/// Phase 3: apply offsets within chunks (parallel).
251pub fn parallel_exclusive_scan(data: &[f64]) -> Vec<f64> {
252    let n = data.len();
253    if n == 0 {
254        return Vec::new();
255    }
256    let chunk_size = (n / rayon::current_num_threads().max(1)).max(64);
257    let chunks: Vec<&[f64]> = data.chunks(chunk_size).collect();
258    let chunk_sums: Vec<f64> = chunks
259        .par_iter()
260        .map(|chunk| chunk.iter().copied().sum())
261        .collect();
262    let mut offsets = Vec::with_capacity(chunks.len());
263    let mut acc = 0.0;
264    for &cs in &chunk_sums {
265        offsets.push(acc);
266        acc += cs;
267    }
268    let result = vec![0.0; n];
269    chunks.par_iter().enumerate().for_each(|(ci, chunk)| {
270        let base = ci * chunk_size;
271        let offset = offsets[ci];
272        let mut local_acc = offset;
273        let result_ptr = result.as_ptr() as *mut f64;
274        for (k, &v) in chunk.iter().enumerate() {
275            unsafe {
276                *result_ptr.add(base + k) = local_acc;
277            }
278            local_acc += v;
279        }
280    });
281    result
282}
283/// Parallel inclusive prefix sum.
284pub fn parallel_inclusive_scan(data: &[f64]) -> Vec<f64> {
285    let n = data.len();
286    if n == 0 {
287        return Vec::new();
288    }
289    let chunk_size = (n / rayon::current_num_threads().max(1)).max(64);
290    let chunks: Vec<&[f64]> = data.chunks(chunk_size).collect();
291    let chunk_sums: Vec<f64> = chunks
292        .par_iter()
293        .map(|chunk| chunk.iter().copied().sum())
294        .collect();
295    let mut offsets = Vec::with_capacity(chunks.len());
296    let mut acc = 0.0;
297    for &cs in &chunk_sums {
298        offsets.push(acc);
299        acc += cs;
300    }
301    let result = vec![0.0; n];
302    chunks.par_iter().enumerate().for_each(|(ci, chunk)| {
303        let base = ci * chunk_size;
304        let offset = offsets[ci];
305        let mut local_acc = offset;
306        let result_ptr = result.as_ptr() as *mut f64;
307        for (k, &v) in chunk.iter().enumerate() {
308            local_acc += v;
309            unsafe {
310                *result_ptr.add(base + k) = local_acc;
311            }
312        }
313    });
314    result
315}
316/// Segmented prefix sum: performs exclusive scans within each segment.
317///
318/// `segment_ids` assigns each element to a segment. When the segment ID
319/// changes, the accumulator resets. Segments must be contiguous.
320pub fn segmented_exclusive_scan(data: &[f64], segment_ids: &[usize]) -> Vec<f64> {
321    let n = data.len();
322    let mut result = vec![0.0; n];
323    if n == 0 {
324        return result;
325    }
326    let mut acc = 0.0;
327    let mut current_seg = segment_ids[0];
328    for i in 0..n {
329        if segment_ids[i] != current_seg {
330            current_seg = segment_ids[i];
331            acc = 0.0;
332        }
333        result[i] = acc;
334        acc += data[i];
335    }
336    result
337}
338/// Parallel sort of f64 values (ascending).
339///
340/// Uses Rayon's parallel sort. NaN values are placed at the end.
341pub fn parallel_sort_f64(data: &mut [f64]) {
342    data.par_sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
343}
344/// Parallel argsort: returns indices that would sort `data` in ascending order.
345pub fn parallel_argsort(data: &[f64]) -> Vec<usize> {
346    let mut indices: Vec<usize> = (0..data.len()).collect();
347    indices.par_sort_unstable_by(|&a, &b| {
348        data[a]
349            .partial_cmp(&data[b])
350            .unwrap_or(std::cmp::Ordering::Equal)
351    });
352    indices
353}
354/// Parallel sort by key: sorts `items` based on `key_fn`.
355pub fn parallel_sort_by_key<T: Send>(items: &mut [T], key_fn: impl Fn(&T) -> f64 + Sync + Send) {
356    items.par_sort_unstable_by(|a, b| {
357        let ka = key_fn(a);
358        let kb = key_fn(b);
359        ka.partial_cmp(&kb).unwrap_or(std::cmp::Ordering::Equal)
360    });
361}
362/// Parallel partition: split data into two groups based on a predicate.
363///
364/// Returns `(true_group, false_group)`.
365pub fn parallel_partition<T: Send + Sync + Clone>(
366    data: &[T],
367    predicate: impl Fn(&T) -> bool + Sync + Send,
368) -> (Vec<T>, Vec<T>) {
369    let (left, right): (Vec<_>, Vec<_>) =
370        data.par_iter().cloned().partition(|item| predicate(item));
371    (left, right)
372}
373/// Parallel rank: compute the rank of each element (0-based) in sorted order.
374pub fn parallel_rank(data: &[f64]) -> Vec<usize> {
375    let sorted_indices = parallel_argsort(data);
376    let n = data.len();
377    let mut ranks = vec![0usize; n];
378    for (rank, &idx) in sorted_indices.iter().enumerate() {
379        ranks[idx] = rank;
380    }
381    ranks
382}
383/// Compute a load balance plan for `n` items across `num_workers` workers.
384///
385/// For `Static` strategy, `item_weights` is ignored.
386/// For `Weighted` strategy, items are assigned sequentially to workers to
387/// balance the total weight per worker.
388/// For `Guided` strategy, chunks start large and shrink.
389pub fn compute_load_balance(
390    n: usize,
391    num_workers: usize,
392    strategy: LoadBalanceStrategy,
393    item_weights: Option<&[f64]>,
394) -> LoadBalancePlan {
395    let nw = num_workers.max(1);
396    match strategy {
397        LoadBalanceStrategy::Static => {
398            let chunk = n.div_ceil(nw);
399            let mut ranges = Vec::with_capacity(nw);
400            let mut weights = Vec::with_capacity(nw);
401            for w in 0..nw {
402                let start = w * chunk;
403                let end = ((w + 1) * chunk).min(n);
404                if start < n {
405                    let weight = if let Some(wts) = item_weights {
406                        wts[start..end].iter().sum()
407                    } else {
408                        (end - start) as f64
409                    };
410                    ranges.push(start..end);
411                    weights.push(weight);
412                }
413            }
414            LoadBalancePlan { ranges, weights }
415        }
416        LoadBalanceStrategy::Weighted => {
417            let wts = match item_weights {
418                Some(w) => w,
419                None => {
420                    return compute_load_balance(n, num_workers, LoadBalanceStrategy::Static, None);
421                }
422            };
423            let total_weight: f64 = wts.iter().sum();
424            let target_per_worker = total_weight / nw as f64;
425            let mut ranges = Vec::with_capacity(nw);
426            let mut weights = Vec::with_capacity(nw);
427            let mut start = 0;
428            let mut current_weight = 0.0;
429            for (i, &wi) in wts.iter().enumerate() {
430                current_weight += wi;
431                let workers_remaining = nw - ranges.len();
432                let at_last_worker = workers_remaining == 1;
433                let exceeded_target = current_weight >= target_per_worker && !at_last_worker;
434                if exceeded_target {
435                    ranges.push(start..(i + 1));
436                    weights.push(current_weight);
437                    start = i + 1;
438                    current_weight = 0.0;
439                }
440            }
441            if start < n || ranges.is_empty() {
442                ranges.push(start..n);
443                weights.push(current_weight);
444            }
445            LoadBalancePlan { ranges, weights }
446        }
447        LoadBalanceStrategy::Guided => {
448            let mut ranges = Vec::new();
449            let mut weights = Vec::new();
450            let mut pos = 0;
451            let mut remaining = n;
452            while remaining > 0 {
453                let min_chunk = 1usize;
454                let chunk = (remaining / nw).max(min_chunk).min(remaining);
455                let end = pos + chunk;
456                let weight = if let Some(wts) = item_weights {
457                    wts[pos..end].iter().sum()
458                } else {
459                    chunk as f64
460                };
461                ranges.push(pos..end);
462                weights.push(weight);
463                pos = end;
464                remaining -= chunk;
465            }
466            LoadBalancePlan { ranges, weights }
467        }
468    }
469}
470/// Execute a function in parallel with load-balanced ranges.
471///
472/// Each range is processed as one Rayon task. The function receives
473/// `(worker_id, range)`.
474pub fn execute_balanced(
475    plan: &LoadBalancePlan,
476    f: impl Fn(usize, std::ops::Range<usize>) + Sync + Send,
477) {
478    plan.ranges
479        .par_iter()
480        .enumerate()
481        .for_each(|(worker_id, range)| {
482            f(worker_id, range.clone());
483        });
484}
485/// Parallel map-reduce: map each element, then reduce results.
486///
487/// Combines mapping and reduction in a single parallel pass.
488pub fn parallel_map_reduce<T: Send + Sync>(
489    data: &[T],
490    map_fn: impl Fn(&T) -> f64 + Sync + Send,
491    identity: f64,
492    reduce_fn: impl Fn(f64, f64) -> f64 + Sync + Send,
493) -> f64 {
494    data.par_iter().map(map_fn).reduce(|| identity, reduce_fn)
495}
496/// Parallel histogram: count elements falling into each bin.
497///
498/// Bins are `[min, min+step), [min+step, min+2*step), ...`.
499/// Returns a vector of length `num_bins`.
500pub fn parallel_histogram(data: &[f64], min: f64, max: f64, num_bins: usize) -> Vec<usize> {
501    if num_bins == 0 || max <= min {
502        return vec![0; num_bins];
503    }
504    let step = (max - min) / num_bins as f64;
505    data.par_iter()
506        .fold(
507            || vec![0usize; num_bins],
508            |mut hist, &v| {
509                if v >= min && v < max {
510                    let bin = ((v - min) / step) as usize;
511                    let bin = bin.min(num_bins - 1);
512                    hist[bin] += 1;
513                } else if (v - max).abs() < 1e-15 {
514                    hist[num_bins - 1] += 1;
515                }
516                hist
517            },
518        )
519        .reduce(
520            || vec![0usize; num_bins],
521            |mut a, b| {
522                for (ai, bi) in a.iter_mut().zip(b.iter()) {
523                    *ai += bi;
524                }
525                a
526            },
527        )
528}
529/// Stream compaction: retain only elements satisfying a predicate, returning
530/// a compacted output together with a scatter index map.
531///
532/// Returns `(compacted, scatter_map)` where:
533/// * `compacted` contains the elements `data[i]` for which `pred(data[i])` is true.
534/// * `scatter_map[j]` is the original index `i` of `compacted[j]`.
535///
536/// This mirrors a GPU stream-compaction pass (prefix-sum → scatter).
537pub fn stream_compaction<T: Clone>(data: &[T], pred: impl Fn(&T) -> bool) -> (Vec<T>, Vec<usize>) {
538    let mut compacted = Vec::new();
539    let mut scatter_map = Vec::new();
540    for (i, item) in data.iter().enumerate() {
541        if pred(item) {
542            compacted.push(item.clone());
543            scatter_map.push(i);
544        }
545    }
546    (compacted, scatter_map)
547}
548/// Parallel stream compaction via Rayon.
549///
550/// Each thread builds a local (value, original_index) list and then the
551/// lists are merged in order to preserve a deterministic output.
552pub fn parallel_stream_compaction<T: Clone + Send + Sync>(
553    data: &[T],
554    pred: impl Fn(&T) -> bool + Sync,
555) -> (Vec<T>, Vec<usize>) {
556    use rayon::iter::IndexedParallelIterator;
557    let pairs: Vec<(T, usize)> = data
558        .par_iter()
559        .enumerate()
560        .filter_map(|(i, item)| {
561            if pred(item) {
562                Some((item.clone(), i))
563            } else {
564                None
565            }
566        })
567        .collect();
568    let compacted: Vec<T> = pairs.iter().map(|(v, _)| v.clone()).collect();
569    let scatter_map: Vec<usize> = pairs.iter().map(|(_, i)| *i).collect();
570    (compacted, scatter_map)
571}
572/// Segmented reduction: sum values within each segment independently.
573///
574/// `segment_ids[i]` must be monotonically non-decreasing.
575/// Returns a vector of partial sums, one per distinct segment id.
576///
577/// Example:
578/// ```text
579/// data          = [1, 2, 3, 4, 5, 6]
580/// segment_ids   = [0, 0, 1, 1, 1, 2]
581/// output        = [3, 12, 6]
582/// ```
583pub fn segmented_reduce_sum(data: &[f64], segment_ids: &[usize]) -> Vec<f64> {
584    if data.is_empty() {
585        return Vec::new();
586    }
587    let max_seg = *segment_ids.iter().max().unwrap_or(&0);
588    let mut result = vec![0.0f64; max_seg + 1];
589    for (&v, &s) in data.iter().zip(segment_ids.iter()) {
590        result[s] += v;
591    }
592    result
593}
594/// Segmented reduction: maximum value within each segment.
595pub fn segmented_reduce_max(data: &[f64], segment_ids: &[usize]) -> Vec<f64> {
596    if data.is_empty() {
597        return Vec::new();
598    }
599    let max_seg = *segment_ids.iter().max().unwrap_or(&0);
600    let mut result = vec![f64::NEG_INFINITY; max_seg + 1];
601    for (&v, &s) in data.iter().zip(segment_ids.iter()) {
602        if v > result[s] {
603            result[s] = v;
604        }
605    }
606    result
607}
608/// Segmented reduction: minimum value within each segment.
609pub fn segmented_reduce_min(data: &[f64], segment_ids: &[usize]) -> Vec<f64> {
610    if data.is_empty() {
611        return Vec::new();
612    }
613    let max_seg = *segment_ids.iter().max().unwrap_or(&0);
614    let mut result = vec![f64::INFINITY; max_seg + 1];
615    for (&v, &s) in data.iter().zip(segment_ids.iter()) {
616        if v < result[s] {
617            result[s] = v;
618        }
619    }
620    result
621}
622/// Stable merge sort for f64 values (CPU reference implementation).
623///
624/// Returns a new sorted vector leaving the input unchanged.
625/// NaN values are placed at the end (treated as greater than any finite value).
626pub fn merge_sort_f64(data: &[f64]) -> Vec<f64> {
627    let mut buf = data.to_vec();
628    merge_sort_recurse(&mut buf);
629    buf
630}
631pub(super) fn merge_sort_recurse(data: &mut [f64]) {
632    let n = data.len();
633    if n <= 1 {
634        return;
635    }
636    let mid = n / 2;
637    merge_sort_recurse(&mut data[..mid]);
638    merge_sort_recurse(&mut data[mid..]);
639    let left: Vec<f64> = data[..mid].to_vec();
640    let right: Vec<f64> = data[mid..].to_vec();
641    let (mut l, mut r, mut i) = (0, 0, 0);
642    while l < left.len() && r < right.len() {
643        if left[l]
644            .partial_cmp(&right[r])
645            .unwrap_or(std::cmp::Ordering::Greater)
646            != std::cmp::Ordering::Greater
647        {
648            data[i] = left[l];
649            l += 1;
650        } else {
651            data[i] = right[r];
652            r += 1;
653        }
654        i += 1;
655    }
656    while l < left.len() {
657        data[i] = left[l];
658        l += 1;
659        i += 1;
660    }
661    while r < right.len() {
662        data[i] = right[r];
663        r += 1;
664        i += 1;
665    }
666}
667/// Merge sort returning the sorted permutation (argsort, stable).
668///
669/// `result[k]` is the original index of the k-th smallest element.
670pub fn merge_sort_argsort(data: &[f64]) -> Vec<usize> {
671    let mut indices: Vec<usize> = (0..data.len()).collect();
672    merge_argsort_recurse(data, &mut indices);
673    indices
674}
675pub(super) fn merge_argsort_recurse(data: &[f64], indices: &mut [usize]) {
676    let n = indices.len();
677    if n <= 1 {
678        return;
679    }
680    let mid = n / 2;
681    let (left_idx, right_idx) = indices.split_at_mut(mid);
682    merge_argsort_recurse(data, left_idx);
683    merge_argsort_recurse(data, right_idx);
684    let left: Vec<usize> = left_idx.to_vec();
685    let right: Vec<usize> = right_idx.to_vec();
686    let (mut l, mut r, mut i) = (0, 0, 0);
687    while l < left.len() && r < right.len() {
688        let cmp = data[left[l]]
689            .partial_cmp(&data[right[r]])
690            .unwrap_or(std::cmp::Ordering::Greater);
691        if cmp != std::cmp::Ordering::Greater {
692            indices[i] = left[l];
693            l += 1;
694        } else {
695            indices[i] = right[r];
696            r += 1;
697        }
698        i += 1;
699    }
700    while l < left.len() {
701        indices[i] = left[l];
702        l += 1;
703        i += 1;
704    }
705    while r < right.len() {
706        indices[i] = right[r];
707        r += 1;
708        i += 1;
709    }
710}
711/// Bitonic sort in ascending order.
712///
713/// Works on arrays whose length is a power of two.  Pads the input with
714/// `f64::INFINITY` if needed and trims back afterwards.
715///
716/// This CPU reference mirrors a GPU bitonic sort which operates in
717/// `O(n log² n)` compare-and-swap steps.
718pub fn bitonic_sort(data: &[f64]) -> Vec<f64> {
719    let n = data.len();
720    if n == 0 {
721        return Vec::new();
722    }
723    let mut p = 1usize;
724    while p < n {
725        p <<= 1;
726    }
727    let mut buf: Vec<f64> = data.to_vec();
728    buf.resize(p, f64::INFINITY);
729    let mut k = 2usize;
730    while k <= p {
731        let mut j = k >> 1;
732        while j >= 1 {
733            for i in 0..p {
734                let l = i ^ j;
735                if l > i {
736                    let ascending = (i & k) == 0;
737                    if (ascending && buf[i] > buf[l]) || (!ascending && buf[i] < buf[l]) {
738                        buf.swap(i, l);
739                    }
740                }
741            }
742            j >>= 1;
743        }
744        k <<= 1;
745    }
746    buf.truncate(n);
747    buf
748}
749/// Bitonic sort that returns the original indices (argsort variant).
750///
751/// Pads with `(f64::INFINITY, usize::MAX)` pairs and trims back.
752pub fn bitonic_argsort(data: &[f64]) -> Vec<usize> {
753    let n = data.len();
754    if n == 0 {
755        return Vec::new();
756    }
757    let mut p = 1usize;
758    while p < n {
759        p <<= 1;
760    }
761    let mut buf: Vec<(f64, usize)> = data
762        .iter()
763        .copied()
764        .enumerate()
765        .map(|(i, v)| (v, i))
766        .collect();
767    buf.resize(p, (f64::INFINITY, usize::MAX));
768    let mut k = 2usize;
769    while k <= p {
770        let mut j = k >> 1;
771        while j >= 1 {
772            for i in 0..p {
773                let l = i ^ j;
774                if l > i {
775                    let ascending = (i & k) == 0;
776                    let should_swap =
777                        (ascending && buf[i].0 > buf[l].0) || (!ascending && buf[i].0 < buf[l].0);
778                    if should_swap {
779                        buf.swap(i, l);
780                    }
781                }
782            }
783            j >>= 1;
784        }
785        k <<= 1;
786    }
787    buf.truncate(n);
788    buf.iter().map(|(_, idx)| *idx).collect()
789}
790/// Simulate a work-stealing dispatcher across `num_workers` queues.
791///
792/// `tasks` is divided evenly among workers.  Any worker that finishes early
793/// steals from the most loaded remaining worker.
794/// Returns a `Vec`usize` of length `num_workers` recording the tasks each
795/// worker processed.
796pub fn work_steal_queue<T: Send + Clone>(
797    tasks: Vec<T>,
798    num_workers: usize,
799    _process: impl Fn(&T) + Sync,
800) -> Vec<usize> {
801    let nw = num_workers.max(1);
802    let mut queues: Vec<WorkStealQueue<T>> = (0..nw).map(|_| WorkStealQueue::new()).collect();
803    for (i, task) in tasks.into_iter().enumerate() {
804        queues[i % nw].push(task);
805    }
806    let mut processed = vec![0usize; nw];
807    loop {
808        let mut did_work = false;
809        for w in 0..nw {
810            while let Some(task) = queues[w].pop() {
811                _process(&task);
812                processed[w] += 1;
813                did_work = true;
814            }
815        }
816        let max_len = queues.iter().map(|q| q.len()).max().unwrap_or(0);
817        if max_len == 0 {
818            break;
819        }
820        if did_work {
821            continue;
822        }
823        let victim = queues
824            .iter()
825            .enumerate()
826            .max_by_key(|(_, q)| q.len())
827            .map(|(i, _)| i);
828        let thief = queues
829            .iter()
830            .enumerate()
831            .find(|(_, q)| q.is_empty())
832            .map(|(i, _)| i);
833        if let (Some(v), Some(t)) = (victim, thief) {
834            if v != t {
835                if let Some(task) = queues[v].steal() {
836                    queues[t].push(task);
837                }
838            } else {
839                break;
840            }
841        } else {
842            break;
843        }
844    }
845    processed
846}
847/// Compute a load-balance efficiency metric given per-worker task counts.
848///
849/// Returns a value in `\[0, 1\]`: 1.0 means perfect balance, smaller values
850/// indicate more imbalance.  Defined as `avg_load / max_load`.
851pub fn compute_load_balance_metric(worker_loads: &[usize]) -> f64 {
852    if worker_loads.is_empty() {
853        return 1.0;
854    }
855    let total: usize = worker_loads.iter().sum();
856    let n = worker_loads.len();
857    let avg = total as f64 / n as f64;
858    let max = *worker_loads.iter().max().unwrap_or(&1) as f64;
859    if max < 1e-15 {
860        return 1.0;
861    }
862    avg / max
863}
864/// Suggest an optimal chunk size for `n` work items across `num_workers`
865/// workers, targeting at least `min_chunks_per_worker` chunks per worker.
866pub fn suggest_chunk_size(n: usize, num_workers: usize, min_chunks_per_worker: usize) -> usize {
867    let nw = num_workers.max(1);
868    let chunks = (nw * min_chunks_per_worker).max(1);
869    n.div_ceil(chunks).max(1)
870}
871/// Parallel merge sort for `f64` slices using Rayon.
872///
873/// Splits the array recursively.  Below `SERIAL_THRESHOLD` elements the
874/// standard library sort is used.  Above that the two halves are sorted in
875/// parallel and then merged sequentially.
876pub fn merge_sort_parallel(data: &[f64]) -> Vec<f64> {
877    pub(super) const SERIAL_THRESHOLD: usize = 256;
878    let n = data.len();
879    if n <= 1 {
880        return data.to_vec();
881    }
882    if n <= SERIAL_THRESHOLD {
883        let mut v = data.to_vec();
884        v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
885        return v;
886    }
887    let mid = n / 2;
888    let (left_slice, right_slice) = data.split_at(mid);
889    let (left_sorted, right_sorted) = rayon::join(
890        || merge_sort_parallel(left_slice),
891        || merge_sort_parallel(right_slice),
892    );
893    merge_two_sorted(&left_sorted, &right_sorted)
894}
895/// Merge two sorted `f64` slices into one sorted `Vec`f64`.
896pub fn merge_two_sorted(a: &[f64], b: &[f64]) -> Vec<f64> {
897    let mut result = Vec::with_capacity(a.len() + b.len());
898    let (mut i, mut j) = (0, 0);
899    while i < a.len() && j < b.len() {
900        if a[i] <= b[j] {
901            result.push(a[i]);
902            i += 1;
903        } else {
904            result.push(b[j]);
905            j += 1;
906        }
907    }
908    result.extend_from_slice(&a[i..]);
909    result.extend_from_slice(&b[j..]);
910    result
911}