Skip to main content

strided_basic/erased/
arg_reduce.rs

1//! Dtype-erased `argmax` / `argmin` along one axis.
2
3use super::line::{for_each_unit, LineLayout, UnitKernel, UnitPtr, PANEL};
4use super::{
5    check_reduce_layout_offset_arithmetic, checked_total_len, reduce_uninit_writer, reduce_writer,
6    ReduceWriter,
7};
8use crate::erased_common::{check_dtype, validate_uninit_no_overlap};
9use crate::*;
10use num_complex::{Complex32, Complex64};
11
12/// Runtime operation of an [`ErasedArgReducePlan`].
13///
14/// # Examples
15///
16/// ```
17/// use strided_basic::ArgReduceOp;
18/// assert_ne!(ArgReduceOp::Max, ArgReduceOp::MaxAbs);
19/// ```
20#[non_exhaustive]
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ArgReduceOp {
23    /// Index of the largest value. Real dtypes only.
24    Max,
25    /// Index of the smallest value. Real dtypes only.
26    Min,
27    /// Index of the largest magnitude (`|x|`, the modulus for complex input).
28    MaxAbs,
29    /// Index of the smallest magnitude (`|x|`, the modulus for complex input).
30    MinAbs,
31}
32
33impl ArgReduceOp {
34    #[inline]
35    fn label(self) -> &'static str {
36        match self {
37            Self::Max => "argmax",
38            Self::Min => "argmin",
39            Self::MaxAbs => "argmax_abs",
40            Self::MinAbs => "argmin_abs",
41        }
42    }
43}
44
45/// Dtype-erased `argmax` / `argmin` along one axis.
46///
47/// The destination holds one index per line: its dimensions are the source
48/// dimensions with `axis` removed (batch axes are preserved in order), and its
49/// dtype is the index dtype, `i32` or `i64`. Indices count from the start of
50/// the logical axis, whatever the sign of its stride.
51///
52/// Supported source dtypes are `f32`, `f64`, `i32` and `i64` for every
53/// [`ArgReduceOp`], and `c32` / `c64` for the magnitude variants
54/// ([`ArgReduceOp::MaxAbs`], [`ArgReduceOp::MinAbs`]) only.
55///
56/// # Ties, NaN and magnitudes
57///
58/// * Ties resolve to the **lowest index**. `-0.0` and `+0.0` compare equal,
59///   so they tie.
60/// * NaN propagates like [`ReduceOp::Max`]: if a line contains a NaN, the
61///   result is the index of its **first** NaN, for both the max and the min
62///   variants. A complex element is NaN when either component is NaN. This
63///   matches NumPy, PyTorch and JAX.
64/// * Infinities order normally; `+inf` beats every finite value for `Max`.
65/// * The complex magnitude is the overflow-safe modulus (`hypot` of the
66///   components), so values near the top of the exponent range still order
67///   correctly. A component of `±inf` makes the magnitude `+inf` unless the
68///   other component is NaN.
69/// * The integer magnitude is the unsigned absolute value, so `i32::MIN` is
70///   larger than `i32::MAX`.
71///
72/// The result does not depend on the layout, execution context or thread
73/// count.
74///
75/// # Examples
76///
77/// ```
78/// use strided_basic::{
79///     ArgReduceOp, ErasedArgReducePlan, ErasedRawStridedMut, ErasedRawStridedRef, ExecContext,
80///     KernelDType,
81/// };
82///
83/// // Column-major 3 x 2 matrix; argmax down each column.
84/// let src = [1.0_f64, 5.0, 5.0, -2.0, -7.0, 3.0];
85/// let mut out = [0_i64; 2];
86/// let plan = ErasedArgReducePlan::compile(
87///     KernelDType::F64,
88///     KernelDType::I64,
89///     ArgReduceOp::Max,
90///     &[3, 2],
91///     &[1, 3],
92///     &[2],
93///     &[1],
94///     0,
95/// )
96/// .unwrap();
97/// let src_ref = ErasedRawStridedRef::from_slice(&src, &[3, 2], &[1, 3], 0).unwrap();
98/// let mut dest = ErasedRawStridedMut::from_slice_mut(&mut out, &[2], &[1], 0).unwrap();
99/// plan.execute(&ExecContext::serial(), &mut dest, &src_ref).unwrap();
100/// assert_eq!(out, [1, 2]); // lowest index wins the 5.0 tie
101/// ```
102#[derive(Clone, Debug)]
103pub struct ErasedArgReducePlan {
104    dtype: KernelDType,
105    index_dtype: KernelDType,
106    op: ArgReduceOp,
107    src_dims: crate::inline::InlineVec<usize>,
108    src_strides: crate::inline::InlineVec<isize>,
109    dest_dims: crate::inline::InlineVec<usize>,
110    dest_strides: crate::inline::InlineVec<isize>,
111    layout: LineLayout,
112}
113
114impl ErasedArgReducePlan {
115    /// Validate and store an arg-reduction plan for fixed layouts.
116    ///
117    /// `dest_dims` must equal `src_dims` with `axis` removed.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use strided_basic::{ArgReduceOp, ErasedArgReducePlan, KernelDType};
123    /// let plan = ErasedArgReducePlan::compile(
124    ///     KernelDType::C64, KernelDType::I32, ArgReduceOp::MaxAbs, &[4, 3], &[3, 1], &[3], &[1], 0,
125    /// )
126    /// .unwrap();
127    /// assert_eq!(plan.index_dtype(), KernelDType::I32);
128    /// // Complex input has no order without the magnitude.
129    /// assert!(ErasedArgReducePlan::compile(
130    ///     KernelDType::C64, KernelDType::I64, ArgReduceOp::Max, &[4], &[1], &[], &[], 0,
131    /// )
132    /// .is_err());
133    /// // An empty axis has no index to return.
134    /// assert!(ErasedArgReducePlan::compile(
135    ///     KernelDType::F64, KernelDType::I64, ArgReduceOp::Max, &[0, 2], &[1, 1], &[2], &[1], 0,
136    /// )
137    /// .is_err());
138    /// ```
139    ///
140    /// # Errors
141    ///
142    /// * `UnsupportedDType` for a `bool` source or an index dtype other than
143    ///   `i32` / `i64`;
144    /// * `UnsupportedOp` for `Max` / `Min` on complex input, and for a
145    ///   zero-length `axis`, which has no index to return;
146    /// * `InvalidAxis`, `StrideLengthMismatch`, `ShapeMismatch` and
147    ///   `NonInjectiveOutputLayout` for inconsistent layouts;
148    /// * `OffsetOverflow` when a layout's offsets are not representable, or
149    ///   when the axis is too long for an `i32` index.
150    #[allow(clippy::too_many_arguments)]
151    pub fn compile(
152        dtype: KernelDType,
153        index_dtype: KernelDType,
154        op: ArgReduceOp,
155        src_dims: &[usize],
156        src_strides: &[isize],
157        dest_dims: &[usize],
158        dest_strides: &[isize],
159        axis: usize,
160    ) -> Result<Self> {
161        check_arg_dtype(dtype, op)?;
162        if !matches!(index_dtype, KernelDType::I32 | KernelDType::I64) {
163            return Err(StridedError::UnsupportedDType {
164                dtype: index_dtype.label(),
165            });
166        }
167        if src_dims.len() != src_strides.len() || dest_dims.len() != dest_strides.len() {
168            return Err(StridedError::StrideLengthMismatch);
169        }
170        let rank = src_dims.len();
171        if axis >= rank {
172            return Err(StridedError::InvalidAxis { axis, rank });
173        }
174        let expected: crate::inline::InlineVec<usize> = (0..rank)
175            .filter(|&a| a != axis)
176            .map(|a| src_dims[a])
177            .collect();
178        if dest_dims != expected.as_slice() {
179            return Err(StridedError::ShapeMismatch(
180                dest_dims.to_vec(),
181                expected.to_vec(),
182            ));
183        }
184        if src_dims[axis] == 0 {
185            return Err(StridedError::UnsupportedOp {
186                op: "argmax/argmin over a zero-length axis",
187                dtype: dtype.label(),
188            });
189        }
190        if index_dtype == KernelDType::I32 && i32::try_from(src_dims[axis] - 1).is_err() {
191            return Err(StridedError::OffsetOverflow);
192        }
193        checked_total_len(src_dims)?;
194        check_reduce_layout_offset_arithmetic(src_dims, src_strides)?;
195        check_reduce_layout_offset_arithmetic(dest_dims, dest_strides)?;
196        if !crate::layout_check::is_injective_layout(dest_dims, dest_strides) {
197            return Err(StridedError::NonInjectiveOutputLayout);
198        }
199        let layout = LineLayout::compile(src_dims, src_strides, dest_strides, 0, axis, false)?;
200        Ok(Self {
201            dtype,
202            index_dtype,
203            op,
204            src_dims: src_dims.into(),
205            src_strides: src_strides.into(),
206            dest_dims: dest_dims.into(),
207            dest_strides: dest_strides.into(),
208            layout,
209        })
210    }
211
212    /// Source element dtype.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use strided_basic::{ArgReduceOp, ErasedArgReducePlan, KernelDType};
218    /// let plan = ErasedArgReducePlan::compile(
219    ///     KernelDType::F32, KernelDType::I64, ArgReduceOp::Min, &[2], &[1], &[], &[], 0,
220    /// )
221    /// .unwrap();
222    /// assert_eq!(plan.dtype(), KernelDType::F32);
223    /// ```
224    #[inline]
225    pub fn dtype(&self) -> KernelDType {
226        self.dtype
227    }
228
229    /// Destination index dtype (`i32` or `i64`).
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use strided_basic::{ArgReduceOp, ErasedArgReducePlan, KernelDType};
235    /// let plan = ErasedArgReducePlan::compile(
236    ///     KernelDType::F32, KernelDType::I64, ArgReduceOp::Min, &[2], &[1], &[], &[], 0,
237    /// )
238    /// .unwrap();
239    /// assert_eq!(plan.index_dtype(), KernelDType::I64);
240    /// ```
241    #[inline]
242    pub fn index_dtype(&self) -> KernelDType {
243        self.index_dtype
244    }
245
246    /// Arg-reduction operation.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// use strided_basic::{ArgReduceOp, ErasedArgReducePlan, KernelDType};
252    /// let plan = ErasedArgReducePlan::compile(
253    ///     KernelDType::F32, KernelDType::I64, ArgReduceOp::Min, &[2], &[1], &[], &[], 0,
254    /// )
255    /// .unwrap();
256    /// assert_eq!(plan.op(), ArgReduceOp::Min);
257    /// ```
258    #[inline]
259    pub fn op(&self) -> ArgReduceOp {
260        self.op
261    }
262
263    fn check_layouts(
264        &self,
265        dest_dims: &[usize],
266        dest_strides: &[isize],
267        src: &ErasedRawStridedRef<'_>,
268    ) -> Result<()> {
269        if src.dims() != self.src_dims.as_slice()
270            || src.strides() != self.src_strides.as_slice()
271            || dest_dims != self.dest_dims.as_slice()
272            || dest_strides != self.dest_strides.as_slice()
273        {
274            return Err(StridedError::PlanLayoutMismatch);
275        }
276        Ok(())
277    }
278
279    /// Execute into an initialized index destination.
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use strided_basic::{
285    ///     ArgReduceOp, ErasedArgReducePlan, ErasedRawStridedMut, ErasedRawStridedRef,
286    ///     ExecContext, KernelDType,
287    /// };
288    /// let src = [3_i32, -9, 9, 1];
289    /// let mut out = [0_i32; 1];
290    /// let plan = ErasedArgReducePlan::compile(
291    ///     KernelDType::I32, KernelDType::I32, ArgReduceOp::MaxAbs, &[4], &[1], &[], &[], 0,
292    /// )
293    /// .unwrap();
294    /// let src_ref = ErasedRawStridedRef::from_slice(&src, &[4], &[1], 0).unwrap();
295    /// let mut dest = ErasedRawStridedMut::from_slice_mut(&mut out, &[], &[], 0).unwrap();
296    /// plan.execute(&ExecContext::serial(), &mut dest, &src_ref).unwrap();
297    /// assert_eq!(out, [1]);
298    /// ```
299    ///
300    /// # Errors
301    ///
302    /// Returns `DTypeMismatch` or `PlanLayoutMismatch` when a descriptor does
303    /// not match the plan, before any destination write.
304    pub fn execute(
305        &self,
306        ctx: &ExecContext,
307        dest: &mut ErasedRawStridedMut<'_>,
308        src: &ErasedRawStridedRef<'_>,
309    ) -> Result<()> {
310        check_dtype(self.index_dtype, dest.dtype())?;
311        check_dtype(self.dtype, src.dtype())?;
312        self.check_layouts(dest.dims(), dest.strides(), src)?;
313        match self.index_dtype {
314            KernelDType::I32 => {
315                let mut writer = reduce_writer::<i32>(dest)?;
316                self.dispatch_dtype(ctx, &mut writer, src)
317            }
318            _ => {
319                let mut writer = reduce_writer::<i64>(dest)?;
320                self.dispatch_dtype(ctx, &mut writer, src)
321            }
322        }
323    }
324
325    /// Execute into an uninitialized index destination.
326    ///
327    /// On success every reachable destination element is written; validation
328    /// errors are returned before any write.
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// use core::mem::MaybeUninit;
334    /// use num_complex::Complex64;
335    /// use strided_basic::{
336    ///     ArgReduceOp, ErasedArgReducePlan, ErasedRawStridedPtr, ErasedRawStridedRef,
337    ///     ErasedRawStridedUninitMut, ExecContext, KernelDType,
338    /// };
339    /// let src = [Complex64::new(3.0, 4.0), Complex64::new(0.0, 6.0)];
340    /// let mut out = [MaybeUninit::<i64>::uninit()];
341    /// let plan = ErasedArgReducePlan::compile(
342    ///     KernelDType::C64, KernelDType::I64, ArgReduceOp::MinAbs, &[2], &[1], &[], &[], 0,
343    /// )
344    /// .unwrap();
345    /// let src_ref = ErasedRawStridedRef::from_slice(&src, &[2], &[1], 0).unwrap();
346    /// let src_ptr = ErasedRawStridedPtr::from_ref(&src_ref);
347    /// let mut dest = ErasedRawStridedUninitMut::from_uninit_slice(&mut out, &[], &[], 0).unwrap();
348    /// plan.execute_uninit(&ExecContext::serial(), &mut dest, &src_ptr).unwrap();
349    /// assert_eq!(unsafe { out[0].assume_init() }, 0);
350    /// ```
351    ///
352    /// # Errors
353    ///
354    /// As [`Self::execute`], plus `OverlappingInputOutput` when the source
355    /// overlaps the destination allocation.
356    pub fn execute_uninit(
357        &self,
358        ctx: &ExecContext,
359        dest: &mut ErasedRawStridedUninitMut<'_>,
360        src: &ErasedRawStridedPtr<'_>,
361    ) -> Result<()> {
362        check_dtype(self.index_dtype, dest.dtype())?;
363        check_dtype(self.dtype, src.dtype())?;
364        validate_uninit_no_overlap(dest, src, 0)?;
365        // SAFETY: the owning erased entry rejected all input/output overlap before conversion.
366        let src = unsafe { src.try_as_ref_after_no_overlap() }?;
367        self.check_layouts(dest.dims(), dest.strides(), &src)?;
368        match self.index_dtype {
369            KernelDType::I32 => {
370                let mut writer = reduce_uninit_writer::<i32>(dest)?;
371                self.dispatch_dtype(ctx, &mut writer, &src)
372            }
373            _ => {
374                let mut writer = reduce_uninit_writer::<i64>(dest)?;
375                self.dispatch_dtype(ctx, &mut writer, &src)
376            }
377        }
378    }
379
380    fn dispatch_dtype<I, W>(
381        &self,
382        ctx: &ExecContext,
383        dest: &mut W,
384        src: &ErasedRawStridedRef<'_>,
385    ) -> Result<()>
386    where
387        I: ArgIndex,
388        W: ReduceWriter<I>,
389    {
390        // The dtype and op are matched once per execution; each loop is
391        // monomorphized for one (dtype, key, direction, index) combination.
392        macro_rules! real {
393            ($ty:ty) => {
394                match self.op {
395                    ArgReduceOp::Max => self.run::<$ty, I, W, Plain, Greater>(ctx, dest, src),
396                    ArgReduceOp::Min => self.run::<$ty, I, W, Plain, Less>(ctx, dest, src),
397                    ArgReduceOp::MaxAbs => {
398                        self.run::<$ty, I, W, Magnitude, Greater>(ctx, dest, src)
399                    }
400                    ArgReduceOp::MinAbs => self.run::<$ty, I, W, Magnitude, Less>(ctx, dest, src),
401                }
402            };
403        }
404        macro_rules! complex {
405            ($ty:ty) => {
406                match self.op {
407                    ArgReduceOp::MaxAbs => {
408                        self.run::<$ty, I, W, Magnitude, Greater>(ctx, dest, src)
409                    }
410                    ArgReduceOp::MinAbs => self.run::<$ty, I, W, Magnitude, Less>(ctx, dest, src),
411                    // INVARIANT: check_arg_dtype rejects ordered complex ops at compile.
412                    ArgReduceOp::Max | ArgReduceOp::Min => Err(StridedError::UnsupportedOp {
413                        op: self.op.label(),
414                        dtype: self.dtype.label(),
415                    }),
416                }
417            };
418        }
419        match self.dtype {
420            KernelDType::F32 => real!(f32),
421            KernelDType::F64 => real!(f64),
422            KernelDType::I32 => real!(i32),
423            KernelDType::I64 => real!(i64),
424            KernelDType::C32 => complex!(Complex32),
425            KernelDType::C64 => complex!(Complex64),
426            _ => Err(StridedError::UnsupportedDType {
427                dtype: self.dtype.label(),
428            }),
429        }
430    }
431
432    fn run<T, I, W, F, D>(
433        &self,
434        ctx: &ExecContext,
435        dest: &mut W,
436        src: &ErasedRawStridedRef<'_>,
437    ) -> Result<()>
438    where
439        T: KernelStorageElement + MaybeSendSync,
440        I: ArgIndex,
441        W: ReduceWriter<I>,
442        F: KeyFn<T>,
443        D: Direction,
444    {
445        let layout = &self.layout;
446        let source = UnitPtr(src.data_as::<T>()?.as_ptr() as *mut T);
447        // SAFETY: the validated writer owns the destination allocation.
448        let target = UnitPtr(unsafe { dest.ptr() });
449        let n = layout.axis_len;
450        let ss = layout.src_axis_stride;
451        let lane = layout.dest_lane_stride;
452        let kernel = ArgUnit::<T, I, F, D> {
453            source,
454            target,
455            n,
456            ss,
457            lane,
458            _ops: core::marker::PhantomData,
459        };
460        // INVARIANT: (1) compile checked the signed source/destination spans
461        // and every cursor step/reset, and rejected an empty axis; (2) the raw
462        // descriptors validated every reachable offset; (3) execute checked
463        // exact plan-layout equality. Units own disjoint outputs.
464        // SAFETY: the three-link invariant above.
465        unsafe { for_each_unit(ctx, layout, src.offset(), dest.offset(), kernel) }
466    }
467}
468
469/// Per-unit arg-reduction kernel of one execution.
470struct ArgUnit<T, I, F, D> {
471    source: UnitPtr<T>,
472    target: UnitPtr<I>,
473    n: usize,
474    ss: isize,
475    lane: isize,
476    _ops: core::marker::PhantomData<fn() -> (F, D)>,
477}
478
479impl<T, I, F, D> Clone for ArgUnit<T, I, F, D> {
480    fn clone(&self) -> Self {
481        *self
482    }
483}
484impl<T, I, F, D> Copy for ArgUnit<T, I, F, D> {}
485
486impl<T, I, F, D> UnitKernel for ArgUnit<T, I, F, D>
487where
488    T: KernelStorageElement + MaybeSendSync,
489    I: ArgIndex,
490    F: KeyFn<T>,
491    D: Direction,
492{
493    #[inline(always)]
494    unsafe fn unit(self, so: isize, d_o: isize, width: usize) {
495        let Self {
496            source,
497            target,
498            n,
499            ss,
500            lane,
501            ..
502        } = self;
503        // SAFETY: the caller passes offsets of the validated layout.
504        unsafe {
505            if width == 1 {
506                let index = arg_line::<T, F, D>(source.get(), so, ss, n);
507                target.get().offset(d_o).write(I::from_index(index));
508            } else {
509                arg_panel::<T, I, F, D>(source.get(), so, ss, n, target.get(), d_o, lane, width);
510            }
511        }
512    }
513}
514
515fn check_arg_dtype(dtype: KernelDType, op: ArgReduceOp) -> Result<()> {
516    match dtype {
517        KernelDType::F32 | KernelDType::F64 | KernelDType::I32 | KernelDType::I64 => Ok(()),
518        KernelDType::C32 | KernelDType::C64 => match op {
519            ArgReduceOp::MaxAbs | ArgReduceOp::MinAbs => Ok(()),
520            ArgReduceOp::Max | ArgReduceOp::Min => Err(StridedError::UnsupportedOp {
521                op: op.label(),
522                dtype: dtype.label(),
523            }),
524        },
525        _ => Err(StridedError::UnsupportedDType {
526            dtype: dtype.label(),
527        }),
528    }
529}
530
531/// Index element written to the destination.
532pub(super) trait ArgIndex: KernelStorageElement + MaybeSendSync {
533    fn from_index(index: usize) -> Self;
534}
535
536impl ArgIndex for i32 {
537    #[inline(always)]
538    fn from_index(index: usize) -> Self {
539        // INVARIANT: compile rejected axes whose last index exceeds i32::MAX.
540        index as i32
541    }
542}
543
544impl ArgIndex for i64 {
545    #[inline(always)]
546    fn from_index(index: usize) -> Self {
547        // INVARIANT: an addressable axis length fits in i64.
548        index as i64
549    }
550}
551
552/// Comparable key of an element.
553pub(super) trait OrderKey: Copy + PartialEq {
554    /// Whether the key is NaN (never for integer keys).
555    fn key_is_nan(self) -> bool;
556    /// Whether `candidate` replaces `best` for a maximum: strictly greater,
557    /// or the first NaN.
558    fn beats_max(candidate: Self, best: Self) -> bool;
559    /// Whether `candidate` replaces `best` for a minimum: strictly less, or
560    /// the first NaN.
561    fn beats_min(candidate: Self, best: Self) -> bool;
562}
563
564macro_rules! impl_float_key {
565    ($($ty:ty),*) => {$(
566        impl OrderKey for $ty {
567            #[inline(always)]
568            fn key_is_nan(self) -> bool {
569                self.is_nan()
570            }
571            #[inline(always)]
572            fn beats_max(candidate: Self, best: Self) -> bool {
573                // Select form without a data dependent branch; a NaN `best`
574                // is never replaced, so the first NaN wins.
575                (candidate > best) | (candidate.is_nan() & !best.is_nan())
576            }
577            #[inline(always)]
578            fn beats_min(candidate: Self, best: Self) -> bool {
579                (candidate < best) | (candidate.is_nan() & !best.is_nan())
580            }
581        }
582    )*};
583}
584impl_float_key!(f32, f64);
585
586macro_rules! impl_int_key {
587    ($($ty:ty),*) => {$(
588        impl OrderKey for $ty {
589            #[inline(always)]
590            fn key_is_nan(self) -> bool {
591                false
592            }
593            #[inline(always)]
594            fn beats_max(candidate: Self, best: Self) -> bool {
595                candidate > best
596            }
597            #[inline(always)]
598            fn beats_min(candidate: Self, best: Self) -> bool {
599                candidate < best
600            }
601        }
602    )*};
603}
604impl_int_key!(i32, i64, u32, u64);
605
606/// Maps an element to its comparison key.
607pub(super) trait KeyFn<T> {
608    type Key: OrderKey;
609    fn key(value: T) -> Self::Key;
610}
611
612/// The value itself (real dtypes).
613pub(super) struct Plain;
614/// The magnitude `|x|`.
615pub(super) struct Magnitude;
616
617macro_rules! impl_plain_key {
618    ($($ty:ty),*) => {$(
619        impl KeyFn<$ty> for Plain {
620            type Key = $ty;
621            #[inline(always)]
622            fn key(value: $ty) -> $ty {
623                value
624            }
625        }
626    )*};
627}
628impl_plain_key!(f32, f64, i32, i64);
629
630impl KeyFn<f32> for Magnitude {
631    type Key = f32;
632    #[inline(always)]
633    fn key(value: f32) -> f32 {
634        value.abs()
635    }
636}
637impl KeyFn<f64> for Magnitude {
638    type Key = f64;
639    #[inline(always)]
640    fn key(value: f64) -> f64 {
641        value.abs()
642    }
643}
644impl KeyFn<i32> for Magnitude {
645    type Key = u32;
646    #[inline(always)]
647    fn key(value: i32) -> u32 {
648        value.unsigned_abs()
649    }
650}
651impl KeyFn<i64> for Magnitude {
652    type Key = u64;
653    #[inline(always)]
654    fn key(value: i64) -> u64 {
655        value.unsigned_abs()
656    }
657}
658impl KeyFn<Complex32> for Magnitude {
659    type Key = f32;
660    #[inline(always)]
661    fn key(value: Complex32) -> f32 {
662        // `hypot` returns +inf for an infinite component even when the other
663        // is NaN; the NaN test keeps any NaN component a NaN key.
664        if value.re.is_nan() || value.im.is_nan() {
665            f32::NAN
666        } else {
667            value.re.hypot(value.im)
668        }
669    }
670}
671impl KeyFn<Complex64> for Magnitude {
672    type Key = f64;
673    #[inline(always)]
674    fn key(value: Complex64) -> f64 {
675        if value.re.is_nan() || value.im.is_nan() {
676            f64::NAN
677        } else {
678            value.re.hypot(value.im)
679        }
680    }
681}
682
683/// Maximum or minimum, fixed at compile time.
684pub(super) trait Direction {
685    fn beats<K: OrderKey>(candidate: K, best: K) -> bool;
686}
687pub(super) struct Greater;
688pub(super) struct Less;
689impl Direction for Greater {
690    #[inline(always)]
691    fn beats<K: OrderKey>(candidate: K, best: K) -> bool {
692        K::beats_max(candidate, best)
693    }
694}
695impl Direction for Less {
696    #[inline(always)]
697    fn beats<K: OrderKey>(candidate: K, best: K) -> bool {
698        K::beats_min(candidate, best)
699    }
700}
701
702/// Independent lanes of the contiguous winner search.
703const ARG_LANES: usize = 8;
704
705/// Index of the winning element of one line.
706///
707/// A unit-stride line runs two passes: a lane-parallel search for the winning
708/// key (NaN if the line has one), then a scan for the first element whose key
709/// equals it (or is NaN). Equal keys tie, so the second pass returns the
710/// lowest index of the winner, exactly as the sequential scan used for
711/// strided lines.
712///
713/// # Safety
714///
715/// Every `src.offset(so + k * ss)` for `k < n` must be readable, and `n > 0`.
716#[inline(always)]
717unsafe fn arg_line<T, F, D>(src: *const T, so: isize, ss: isize, n: usize) -> usize
718where
719    T: Copy,
720    F: KeyFn<T>,
721    D: Direction,
722{
723    // SAFETY: the caller guarantees every visited offset and `n > 0`; a unit
724    // stride line is `n` contiguous elements.
725    unsafe {
726        if ss == 1 {
727            let values = core::slice::from_raw_parts(src.offset(so), n);
728            let mut lanes = [F::key(values[0]); ARG_LANES];
729            let mut chunks = values.chunks_exact(ARG_LANES);
730            for chunk in &mut chunks {
731                for (lane, &value) in lanes.iter_mut().zip(chunk) {
732                    let key = F::key(value);
733                    *lane = if D::beats(key, *lane) { key } else { *lane };
734                }
735            }
736            // Which NaN or which signed zero wins here does not matter: the
737            // second pass matches by NaN-ness or by equality.
738            let mut winner = lanes[0];
739            let tail = chunks.remainder().iter().map(|&value| F::key(value));
740            for key in lanes[1..].iter().copied().chain(tail) {
741                if D::beats(key, winner) {
742                    winner = key;
743                }
744            }
745            let found = if winner.key_is_nan() {
746                values.iter().position(|&value| F::key(value).key_is_nan())
747            } else {
748                values.iter().position(|&value| F::key(value) == winner)
749            };
750            // INVARIANT: the winner is the key of some element of the line.
751            return found.unwrap_or(0);
752        }
753        let mut best = F::key(src.offset(so).read());
754        let mut best_index = 0;
755        let mut offset = so;
756        for index in 1..n {
757            offset += ss;
758            let key = F::key(src.offset(offset).read());
759            if D::beats(key, best) {
760                best = key;
761                best_index = index;
762            }
763        }
764        best_index
765    }
766}
767
768/// Arg-reduces `width` adjacent lines whose source elements are contiguous
769/// across the lines; output `j` is written at `d_o + j * lane`.
770///
771/// # Safety
772///
773/// As [`arg_line`] for each line `j < width` with source base `so + j`, every
774/// destination offset must be writable, and `width <= PANEL`.
775#[allow(clippy::too_many_arguments)]
776#[inline(always)]
777unsafe fn arg_panel<T, I, F, D>(
778    src: *const T,
779    so: isize,
780    ss: isize,
781    n: usize,
782    dst: *mut I,
783    d_o: isize,
784    lane: isize,
785    width: usize,
786) where
787    T: Copy,
788    I: ArgIndex,
789    F: KeyFn<T>,
790    D: Direction,
791{
792    debug_assert!(width <= PANEL);
793    // SAFETY: the caller guarantees `width` contiguous source elements at
794    // every axis position and the destination offsets.
795    unsafe {
796        let first = core::slice::from_raw_parts(src.offset(so), width);
797        let mut best = [F::key(first[0]); PANEL];
798        let mut best_index = [0usize; PANEL];
799        for (best, &value) in best.iter_mut().zip(first) {
800            *best = F::key(value);
801        }
802        let best = &mut best[..width];
803        let best_index = &mut best_index[..width];
804        let mut offset = so;
805        for index in 1..n {
806            offset += ss;
807            let row = core::slice::from_raw_parts(src.offset(offset), width);
808            for ((best, best_index), &value) in best.iter_mut().zip(best_index.iter_mut()).zip(row)
809            {
810                let key = F::key(value);
811                let take = D::beats(key, *best);
812                *best = if take { key } else { *best };
813                *best_index = if take { index } else { *best_index };
814            }
815        }
816        for (lane_index, &index) in best_index.iter().enumerate() {
817            dst.offset(d_o + lane_index as isize * lane)
818                .write(I::from_index(index));
819        }
820    }
821}