Skip to main content

hermes_simd_core/cow/
math.rs

1//! Mathematical extensions for [`SimdCow`]: norm, normalize, scalar-broadcast ops.
2//!
3//! # Zero-Cost Contract
4//!
5//! All methods monomorphize per `(T, Arch, Align)`. The `Arch` and `Align` ZST markers
6//! are erased at codegen. Scalar-broadcast ops (`add_scalar_cow`, `mul_scalar_cow`, etc.)
7//! allocate exactly one `AlignedVec` output; no intermediate buffer is allocated.
8//!
9//! # Norm and Normalize
10//!
11//! - `norm_sq` — squared Euclidean norm: `∑ self[i]²`. Delegates to `zip_reduce(Dot)`.
12//! - `norm`    — Euclidean norm: `sqrt(norm_sq)`. Uses `T::sqrt_scalar` from `FloatElement`.
13//! - `normalize` — returns a unit-length owned `SimdCow<'static, T, Arch, Align>`.
14//!   Empty or zero-norm vectors return `zeros(self.len())` rather than NaN / division by zero.
15//!
16//! # Safety
17//!
18//! Two obligations recur here. Kernel calls are `#[target_feature]`-gated, and
19//! that precondition holds by construction: a `SimdCow` exists only for an
20//! architecture the host can execute, since its borrowed form comes from
21//! [`SimdView::new`](crate::view::SimdView::new) and its owned constructors
22//! assert the same condition. The second is local — these routines build their
23//! output buffer with `with_capacity` and write it through a raw pointer,
24//! raising the length only once every element is initialized. That avoids both
25//! a zero-fill of a buffer about to be overwritten and any `&mut [T]` spanning
26//! uninitialized elements, so each such site carries a `SAFETY` comment showing
27//! the write coverage. `gather` and `prefix_scan` reserve capacity and fill it
28//! through the view's `*_into_uninit` methods over
29//! [`AlignedVec::spare_capacity_mut`](crate::vec::AlignedVec::spare_capacity_mut),
30//! then raise the length once those report success, so those paths never zero
31//! the buffer either.
32
33use super::SimdCow;
34use crate::align::Alignment;
35use crate::arch::SimdArch;
36use crate::kernel::SimdKernel;
37use crate::ops::{Dot, Sub};
38use crate::scalar::{FloatElement, Scalar};
39use crate::vec::AlignedVec;
40use crate::view::SimdError;
41
42extern crate alloc;
43
44// ---------------------------------------------------------------------------
45// Scalar-broadcast arithmetic
46// ---------------------------------------------------------------------------
47
48impl<'a, T: 'a, Arch, Align> SimdCow<'a, T, Arch, Align>
49where
50    T: Scalar,
51    Arch: SimdArch + SimdKernel<T>,
52    Align: Alignment,
53{
54    /// Add scalar `rhs` to every element: `out[i] = self[i] + rhs`.
55    ///
56    /// One allocation. No second `SimdCow` allocation.
57    #[inline]
58    pub fn add_scalar_cow(&self, rhs: T) -> SimdCow<'static, T, Arch, Align> {
59        broadcast_op::<T, Arch, Align>(
60            self,
61            rhs,
62            |a, b| a + b,
63            |va, vsplat| unsafe { Arch::add(va, vsplat) },
64        )
65    }
66
67    /// Subtract scalar `rhs` from every element: `out[i] = self[i] - rhs`.
68    ///
69    /// One allocation.
70    #[inline]
71    pub fn sub_scalar_cow(&self, rhs: T) -> SimdCow<'static, T, Arch, Align> {
72        broadcast_op::<T, Arch, Align>(
73            self,
74            rhs,
75            |a, b| a - b,
76            |va, vsplat| unsafe { Arch::sub(va, vsplat) },
77        )
78    }
79
80    /// Multiply every element by scalar `rhs`: `out[i] = self[i] * rhs`.
81    ///
82    /// One allocation. For in-place scaling use [`SimdCow::scale_in_place`].
83    #[inline]
84    pub fn mul_scalar_cow(&self, rhs: T) -> SimdCow<'static, T, Arch, Align> {
85        broadcast_op::<T, Arch, Align>(
86            self,
87            rhs,
88            |a, b| a * b,
89            |va, vsplat| unsafe { Arch::mul(va, vsplat) },
90        )
91    }
92
93    /// Elementwise division by scalar `rhs`: `out[i] = self[i] / rhs`.
94    ///
95    /// One allocation.
96    #[inline]
97    pub fn div_scalar_cow(&self, rhs: T) -> SimdCow<'static, T, Arch, Align> {
98        broadcast_op::<T, Arch, Align>(
99            self,
100            rhs,
101            |a, b| a / b,
102            |va, vsplat| unsafe { Arch::div(va, vsplat) },
103        )
104    }
105
106    /// Elementwise division: `out[i] = self[i] / other[i]`.
107    ///
108    /// One allocation.
109    ///
110    /// # Errors
111    /// Returns `SimdError::LengthMismatch` if lengths differ.
112    #[inline]
113    pub fn div_cow(
114        &self,
115        other: &SimdCow<'_, T, Arch, Align>,
116    ) -> Result<SimdCow<'static, T, Arch, Align>, SimdError> {
117        self.zip_cow(other, crate::ops::Div)
118    }
119
120    /// Elementwise subtraction returning owned `SimdCow`, non-method form for symmetry.
121    ///
122    /// Equivalent to `self.sub_cow(other)` — delegates to `zip_cow(Sub)`.
123    ///
124    /// # Errors
125    /// Returns `SimdError::LengthMismatch` if lengths differ.
126    #[inline]
127    pub fn sub_cow_op(
128        &self,
129        other: &SimdCow<'_, T, Arch, Align>,
130    ) -> Result<SimdCow<'static, T, Arch, Align>, SimdError> {
131        self.zip_cow(other, Sub)
132    }
133}
134
135// ---------------------------------------------------------------------------
136// Norm and normalize (float-element only)
137// ---------------------------------------------------------------------------
138
139impl<'a, T: 'a, Arch, Align> SimdCow<'a, T, Arch, Align>
140where
141    T: Scalar + FloatElement,
142    Arch: SimdArch + SimdKernel<T>,
143    Align: Alignment,
144{
145    /// Squared Euclidean norm: `∑ self[i]²`.
146    ///
147    /// Zero-copy: delegates to `zip_reduce(Dot)` which is a single SIMD pass.
148    #[inline]
149    pub fn norm_sq(&self) -> T {
150        let v = self.view();
151        v.zip_reduce(&v, Dot).unwrap_or(T::ZERO)
152    }
153
154    /// Euclidean norm: `√(∑ self[i]²)`.
155    ///
156    /// Delegates to `norm_sq` then `T::sqrt_scalar`.
157    #[inline]
158    pub fn norm(&self) -> T {
159        self.norm_sq().sqrt()
160    }
161
162    /// Returns a unit-length copy: `self / ‖self‖`.
163    ///
164    /// - Empty vector → empty `SimdCow<'static, T, Arch, Align>`.
165    /// - Zero-norm vector → `zeros(self.len())` (safe, no NaN).
166    /// - Otherwise → `self * (1 / ‖self‖)`.
167    ///
168    /// One allocation for the output `AlignedVec`.
169    #[inline]
170    pub fn normalize(&self) -> SimdCow<'static, T, Arch, Align> {
171        let n = self.norm();
172        if n == T::ZERO {
173            return SimdCow::zeros(self.len());
174        }
175        // Compute reciprocal once, multiply — avoids a per-element division.
176        let inv = T::ONE / n;
177        self.mul_scalar_cow(inv)
178    }
179
180    /// Scalar histogram over this cow's values.
181    ///
182    /// Partitions `[lo, hi)` into `n_bins` equal-width bins and counts how many
183    /// elements fall in each bin. Elements outside `[lo, hi)` are ignored.
184    ///
185    /// Returns a `Vec<usize>` of length `n_bins`.
186    ///
187    /// # Panics
188    /// Panics if `n_bins == 0` or `lo >= hi`.
189    #[inline]
190    pub fn histogram_cow(&self, n_bins: usize, lo: T, hi: T) -> alloc::vec::Vec<usize>
191    where
192        T: PartialOrd,
193    {
194        assert!(n_bins > 0, "n_bins must be > 0");
195        assert!(lo < hi, "lo must be < hi");
196
197        // Bin indices are computed in f64: it is a strict superset of every
198        // supported lane precision (f16/bf16/f32/f64), so the index — an
199        // integer output, never narrowed back to `T` — is exact for all `T`.
200        let lo_w = lo.to_f64();
201        let bin_width = (hi.to_f64() - lo_w) / n_bins as f64;
202        let mut counts = alloc::vec![0usize; n_bins];
203
204        for &x in self.as_ref().iter() {
205            if x < lo || x >= hi {
206                continue;
207            }
208            let bin = (((x.to_f64() - lo_w) / bin_width) as usize).min(n_bins - 1);
209            counts[bin] += 1;
210        }
211        counts
212    }
213}
214
215// ---------------------------------------------------------------------------
216// Private helpers
217// ---------------------------------------------------------------------------
218
219/// SIMD broadcast-apply: `out[i] = scalar_op(data[i], rhs)`.
220///
221/// Uses `Arch::splat(rhs)` once, then loops over SIMD vectors with `vector_op`,
222/// followed by a scalar tail loop with `scalar_op`. One allocation.
223#[inline(always)]
224fn broadcast_op<T, Arch, Align>(
225    cow: &SimdCow<'_, T, Arch, Align>,
226    rhs: T,
227    scalar_op: impl Fn(T, T) -> T + Copy,
228    vector_op: impl Fn(Arch::Vector, Arch::Vector) -> Arch::Vector + Copy,
229) -> SimdCow<'static, T, Arch, Align>
230where
231    T: Scalar,
232    Arch: SimdArch + SimdKernel<T>,
233    Align: Alignment,
234{
235    let data = cow.as_ref();
236    let len = data.len();
237    let mut out: AlignedVec<T, Align> = AlignedVec::with_capacity(len);
238
239    let lane_count = Arch::LANE_COUNT;
240    let simd_len = (len / lane_count) * lane_count;
241    let ptr_in = data.as_ptr();
242    let ptr_out = out.as_mut_ptr();
243
244    // SAFETY: `with_capacity(len)` reserved `len` elements and `ptr_in` covers
245    // the same `len` elements, so every access below stays inside its
246    // allocation. The length is raised only after the vector and scalar loops
247    // have together written every element, so no reference spans uninitialized
248    // memory.
249    unsafe {
250        let vsplat = Arch::splat(rhs);
251        let load = |p: *const T| -> Arch::Vector {
252            if crate::align::is_aligned_for_arch::<Arch, Align>() {
253                Arch::load_aligned(p)
254            } else {
255                Arch::load_unaligned(p)
256            }
257        };
258        let store = |p: *mut T, v: Arch::Vector| {
259            if crate::align::is_aligned_for_arch::<Arch, Align>() {
260                Arch::store_aligned(p, v);
261            } else {
262                Arch::store_unaligned(p, v);
263            }
264        };
265        let mut i = 0usize;
266        while i < simd_len {
267            let va = load(ptr_in.add(i));
268            let vr = vector_op(va, vsplat);
269            store(ptr_out.add(i), vr);
270            i += lane_count;
271        }
272        for i in simd_len..len {
273            core::ptr::write(ptr_out.add(i), scalar_op(*ptr_in.add(i), rhs));
274        }
275        out.set_len(len);
276    }
277
278    SimdCow::Owned(out)
279}