Skip to main content

sidereon_core/astro/math/
portable.rs

1//! Portable scalar and linear-algebra boundary helpers.
2//!
3//! `nalgebra` selects architecture-specific matrixmultiply kernels for plain
4//! `f64` dynamic matrices.  `Portable` keeps the same binary64 arithmetic while
5//! making that dispatch ineligible, and routes every transcendental operation
6//! through the Rust `libm` implementation.
7
8use std::fmt;
9use std::num::ParseFloatError;
10use std::ops::{
11    Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign,
12};
13use std::str::FromStr;
14
15use approx::{AbsDiffEq, RelativeEq, UlpsEq};
16use nalgebra::{DMatrix, DVector, Dyn, SMatrix, SVector, SVD};
17use num_traits::{FromPrimitive, Num, One, Signed, ToPrimitive, Zero};
18use simba::scalar::{ComplexField, Field, RealField, SubsetOf};
19use simba::simd::{PrimitiveSimdValue, SimdValue};
20use trust_region_least_squares::trf::{BackendError, HostNumerics};
21
22/// A transparent binary64 value used for portable nalgebra operations.
23#[repr(transparent)]
24#[derive(Clone, Copy, Default, Debug, PartialEq)]
25pub struct Portable(pub f64);
26
27/// Core-owned numerical backend for the public trust-region solver.
28///
29/// The backend deliberately uses the same portable scalar SVD as the core
30/// covariance paths and fixed-order scalar loops for the BLAS-like hooks.  It
31/// is zero-sized, so sharing one value across independent solves is free.
32#[derive(Debug, Clone, Copy, Default)]
33pub(crate) struct PortableNumerics;
34
35impl HostNumerics for PortableNumerics {
36    fn svd(
37        &self,
38        values: &[f64],
39        rows: usize,
40        cols: usize,
41    ) -> Result<(Vec<f64>, Vec<f64>, Vec<f64>), BackendError> {
42        thin_svd(values, rows, cols).map_err(BackendError::Failed)
43    }
44
45    fn dot(&self, lhs: &[f64], rhs: &[f64]) -> Result<Option<f64>, BackendError> {
46        if lhs.len() != rhs.len() {
47            return Err(BackendError::Failed(format!(
48                "dot length mismatch: {} and {}",
49                lhs.len(),
50                rhs.len()
51            )));
52        }
53        let mut result = 0.0;
54        for index in 0..lhs.len() {
55            result += lhs[index] * rhs[index];
56        }
57        Ok(Some(result))
58    }
59
60    fn fortran_matvec(
61        &self,
62        matrix: &[f64],
63        rows: usize,
64        cols: usize,
65        vector: &[f64],
66        transpose: bool,
67    ) -> Result<Option<Vec<f64>>, BackendError> {
68        matvec(matrix, rows, cols, vector, transpose, true)
69    }
70
71    fn row_major_matvec(
72        &self,
73        matrix: &[f64],
74        rows: usize,
75        cols: usize,
76        vector: &[f64],
77        transpose: bool,
78    ) -> Result<Option<Vec<f64>>, BackendError> {
79        matvec(matrix, rows, cols, vector, transpose, false)
80    }
81
82    fn power(&self, values: &[f64], exponent: f64) -> Result<Option<Vec<f64>>, BackendError> {
83        Ok(Some(
84            values
85                .iter()
86                .copied()
87                .map(|value| libm::pow(value, exponent))
88                .collect(),
89        ))
90    }
91
92    fn power_scalar(&self, base: f64, exponent: f64) -> Result<Option<f64>, BackendError> {
93        Ok(Some(libm::pow(base, exponent)))
94    }
95
96    fn log1p(&self, value: f64) -> Result<Option<f64>, BackendError> {
97        Ok(Some(libm::log1p(value)))
98    }
99
100    fn atan(&self, value: f64) -> Result<Option<f64>, BackendError> {
101        Ok(Some(libm::atan(value)))
102    }
103}
104
105fn matvec(
106    matrix: &[f64],
107    rows: usize,
108    cols: usize,
109    vector: &[f64],
110    transpose: bool,
111    column_major: bool,
112) -> Result<Option<Vec<f64>>, BackendError> {
113    let (input_len, output_len) = if transpose {
114        (rows, cols)
115    } else {
116        (cols, rows)
117    };
118    if matrix.len() != rows.saturating_mul(cols) || vector.len() != input_len {
119        return Err(BackendError::Failed(format!(
120            "matvec dimensions {}x{} with vector length {}",
121            rows,
122            cols,
123            vector.len()
124        )));
125    }
126    let mut result = vec![0.0; output_len];
127    for (output, slot) in result.iter_mut().enumerate() {
128        let mut sum = 0.0;
129        for (input, value) in vector.iter().enumerate() {
130            let index = if column_major {
131                if transpose {
132                    output * rows + input
133                } else {
134                    input * rows + output
135                }
136            } else if transpose {
137                input * cols + output
138            } else {
139                output * cols + input
140            };
141            sum += matrix[index] * value;
142        }
143        *slot = sum;
144    }
145    Ok(Some(result))
146}
147
148/// Convert a row-major binary64 slice to a dynamic portable matrix.
149#[inline]
150pub fn matrix_from_row_slice(rows: usize, cols: usize, values: &[f64]) -> DMatrix<Portable> {
151    DMatrix::from_row_slice(
152        rows,
153        cols,
154        &values.iter().copied().map(Portable).collect::<Vec<_>>(),
155    )
156}
157
158/// Convert a binary64 dynamic matrix to the portable scalar representation.
159#[inline]
160pub fn matrix_from_f64(matrix: &DMatrix<f64>) -> DMatrix<Portable> {
161    DMatrix::from_iterator(
162        matrix.nrows(),
163        matrix.ncols(),
164        matrix.iter().copied().map(Portable),
165    )
166}
167
168/// Convert a portable dynamic matrix back to binary64 without changing bits.
169#[inline]
170pub fn matrix_to_f64(matrix: &DMatrix<Portable>) -> DMatrix<f64> {
171    DMatrix::from_iterator(
172        matrix.nrows(),
173        matrix.ncols(),
174        matrix.iter().map(|value| value.0),
175    )
176}
177
178/// Convert a binary64 dynamic vector to the portable scalar representation.
179#[inline]
180pub fn vector_from_f64(vector: &DVector<f64>) -> DVector<Portable> {
181    DVector::from_iterator(vector.len(), vector.iter().copied().map(Portable))
182}
183
184/// Convert a portable dynamic vector back to binary64 without changing bits.
185#[inline]
186pub fn vector_to_f64(vector: &DVector<Portable>) -> DVector<f64> {
187    DVector::from_iterator(vector.len(), vector.iter().map(|value| value.0))
188}
189
190/// Thin SVD of a row-major binary64 matrix using the portable scalar.
191#[allow(clippy::type_complexity)]
192pub fn thin_svd(
193    values: &[f64],
194    rows: usize,
195    cols: usize,
196) -> Result<(Vec<f64>, Vec<f64>, Vec<f64>), String> {
197    if values.len() != rows.saturating_mul(cols) {
198        return Err(format!(
199            "SVD input has length {}, expected {}x{}",
200            values.len(),
201            rows,
202            cols
203        ));
204    }
205    let matrix = matrix_from_row_slice(rows, cols, values);
206    let svd = matrix.svd(true, true);
207    let u = svd
208        .u
209        .ok_or_else(|| "portable SVD did not produce U".to_string())?;
210    let vt = svd
211        .v_t
212        .ok_or_else(|| "portable SVD did not produce V^T".to_string())?;
213    let k = rows.min(cols);
214    let mut u_out = vec![0.0; rows * k];
215    for row in 0..rows {
216        for col in 0..k {
217            u_out[row * k + col] = u[(row, col)].0;
218        }
219    }
220    let s_out = svd.singular_values.iter().map(|value| value.0).collect();
221    let mut vt_out = vec![0.0; k * cols];
222    for row in 0..k {
223        for col in 0..cols {
224            vt_out[row * cols + col] = vt[(row, col)].0;
225        }
226    }
227    Ok((u_out, s_out, vt_out))
228}
229
230/// A dynamic matrix product evaluated by nalgebra with the portable scalar.
231#[inline]
232pub fn product(lhs: &DMatrix<f64>, rhs: &DMatrix<f64>) -> DMatrix<f64> {
233    matrix_to_f64(&(matrix_from_f64(lhs) * matrix_from_f64(rhs)))
234}
235
236/// A dynamic matrix/vector product evaluated by nalgebra with the portable scalar.
237#[inline]
238pub fn product_vector(lhs: &DMatrix<f64>, rhs: &DVector<f64>) -> DVector<f64> {
239    vector_to_f64(&(matrix_from_f64(lhs) * vector_from_f64(rhs)))
240}
241
242/// Fixed-size matrix product evaluated through the portable scalar.
243#[inline]
244pub fn product_fixed<const N: usize>(
245    lhs: &SMatrix<f64, N, N>,
246    rhs: &SMatrix<f64, N, N>,
247) -> SMatrix<f64, N, N> {
248    let lhs_portable = SMatrix::<Portable, N, N>::from_fn(|row, col| Portable(lhs[(row, col)]));
249    let rhs_portable = SMatrix::<Portable, N, N>::from_fn(|row, col| Portable(rhs[(row, col)]));
250    let product = lhs_portable * rhs_portable;
251    SMatrix::from_fn(|row, col| product[(row, col)].0)
252}
253
254/// Solve a dynamic square system through nalgebra's LU decomposition on the
255/// portable scalar.
256#[inline]
257pub fn solve_lu(lhs: &DMatrix<f64>, rhs: &DVector<f64>) -> Option<DVector<f64>> {
258    matrix_from_f64(lhs)
259        .lu()
260        .solve(&vector_from_f64(rhs))
261        .map(|solution| vector_to_f64(&solution))
262}
263
264/// Cholesky solve for a dynamic positive-definite system through `Portable`.
265#[inline]
266pub fn solve_cholesky(lhs: &DMatrix<f64>, rhs: &DVector<f64>) -> Option<DVector<f64>> {
267    matrix_from_f64(lhs)
268        .cholesky()
269        .map(|factor| vector_to_f64(&factor.solve(&vector_from_f64(rhs))))
270}
271
272/// Cholesky lower factor for a dynamic positive-definite matrix through
273/// `Portable`.
274#[inline]
275pub fn cholesky_lower_dynamic(lhs: &DMatrix<f64>) -> Option<DMatrix<f64>> {
276    matrix_from_f64(lhs)
277        .cholesky()
278        .map(|factor| matrix_to_f64(&factor.l()))
279}
280
281/// Symmetric eigendecomposition for a dynamic real matrix through `Portable`.
282#[inline]
283pub fn symmetric_eigen_dynamic(matrix: &DMatrix<f64>) -> (DMatrix<f64>, DVector<f64>) {
284    let eigen = matrix_from_f64(matrix).symmetric_eigen();
285    (
286        matrix_to_f64(&eigen.eigenvectors),
287        vector_to_f64(&eigen.eigenvalues),
288    )
289}
290
291/// Symmetric eigendecomposition for a fixed-size 6x6 real matrix through
292/// `Portable`.
293#[inline]
294pub fn symmetric_eigen6(matrix: &SMatrix<f64, 6, 6>) -> (SMatrix<f64, 6, 6>, SVector<f64, 6>) {
295    let portable = SMatrix::<Portable, 6, 6>::from_fn(|row, col| Portable(matrix[(row, col)]));
296    let eigen = portable.symmetric_eigen();
297    (
298        SMatrix::from_fn(|row, col| eigen.eigenvectors[(row, col)].0),
299        SVector::from_fn(|row, _| eigen.eigenvalues[row].0),
300    )
301}
302
303/// Cholesky lower factor for a fixed-size real matrix through `Portable`.
304#[inline]
305pub fn cholesky_lower<const N: usize>(matrix: &SMatrix<f64, N, N>) -> Option<SMatrix<f64, N, N>> {
306    let portable = SMatrix::<Portable, N, N>::from_fn(|row, col| Portable(matrix[(row, col)]));
307    portable
308        .cholesky()
309        .map(|factor| SMatrix::from_fn(|row, col| factor.l()[(row, col)].0))
310}
311
312/// SVD of a dynamic binary64 matrix through the portable scalar.
313#[inline]
314pub fn svd(matrix: &DMatrix<f64>, compute_u: bool, compute_v: bool) -> SVD<Portable, Dyn, Dyn> {
315    matrix_from_f64(matrix).svd(compute_u, compute_v)
316}
317
318impl Portable {
319    #[inline]
320    pub const fn new(value: f64) -> Self {
321        Self(value)
322    }
323
324    #[inline]
325    pub const fn get(self) -> f64 {
326        self.0
327    }
328}
329
330impl PartialOrd for Portable {
331    #[inline]
332    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
333        self.0.partial_cmp(&other.0)
334    }
335}
336
337impl fmt::Display for Portable {
338    #[inline]
339    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
340        self.0.fmt(formatter)
341    }
342}
343
344impl From<f64> for Portable {
345    #[inline]
346    fn from(value: f64) -> Self {
347        Self(value)
348    }
349}
350
351impl From<Portable> for f64 {
352    #[inline]
353    fn from(value: Portable) -> Self {
354        value.0
355    }
356}
357
358impl Add for Portable {
359    type Output = Self;
360
361    #[inline]
362    fn add(self, rhs: Self) -> Self {
363        Self(self.0 + rhs.0)
364    }
365}
366
367impl AddAssign for Portable {
368    #[inline]
369    fn add_assign(&mut self, rhs: Self) {
370        self.0 += rhs.0;
371    }
372}
373
374impl Sub for Portable {
375    type Output = Self;
376
377    #[inline]
378    fn sub(self, rhs: Self) -> Self {
379        Self(self.0 - rhs.0)
380    }
381}
382
383impl SubAssign for Portable {
384    #[inline]
385    fn sub_assign(&mut self, rhs: Self) {
386        self.0 -= rhs.0;
387    }
388}
389
390impl Mul for Portable {
391    type Output = Self;
392
393    #[inline]
394    fn mul(self, rhs: Self) -> Self {
395        Self(self.0 * rhs.0)
396    }
397}
398
399impl MulAssign for Portable {
400    #[inline]
401    fn mul_assign(&mut self, rhs: Self) {
402        self.0 *= rhs.0;
403    }
404}
405
406impl Div for Portable {
407    type Output = Self;
408
409    #[inline]
410    fn div(self, rhs: Self) -> Self {
411        Self(self.0 / rhs.0)
412    }
413}
414
415impl DivAssign for Portable {
416    #[inline]
417    fn div_assign(&mut self, rhs: Self) {
418        self.0 /= rhs.0;
419    }
420}
421
422impl Rem for Portable {
423    type Output = Self;
424
425    #[inline]
426    fn rem(self, rhs: Self) -> Self {
427        Self(self.0 % rhs.0)
428    }
429}
430
431impl RemAssign for Portable {
432    #[inline]
433    fn rem_assign(&mut self, rhs: Self) {
434        self.0 %= rhs.0;
435    }
436}
437
438impl Neg for Portable {
439    type Output = Self;
440
441    #[inline]
442    fn neg(self) -> Self {
443        Self(-self.0)
444    }
445}
446
447impl Zero for Portable {
448    #[inline]
449    fn zero() -> Self {
450        Self(0.0)
451    }
452
453    #[inline]
454    fn is_zero(&self) -> bool {
455        self.0 == 0.0
456    }
457}
458
459impl One for Portable {
460    #[inline]
461    fn one() -> Self {
462        Self(1.0)
463    }
464}
465
466impl Num for Portable {
467    type FromStrRadixErr = ParseFloatError;
468
469    #[inline]
470    fn from_str_radix(src: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
471        if radix == 10 {
472            src.parse().map(Self)
473        } else {
474            // The scalar is used for binary64 matrices; retain the standard
475            // parser's error type while supporting the radix-independent path.
476            src.parse().map(Self)
477        }
478    }
479}
480
481impl Signed for Portable {
482    #[inline]
483    fn abs(&self) -> Self {
484        Self(libm::fabs(self.0))
485    }
486
487    #[inline]
488    fn abs_sub(&self, other: &Self) -> Self {
489        if self.0 <= other.0 {
490            Self::zero()
491        } else {
492            Self(self.0 - other.0)
493        }
494    }
495
496    #[inline]
497    fn signum(&self) -> Self {
498        Self(self.0.signum())
499    }
500
501    #[inline]
502    fn is_positive(&self) -> bool {
503        self.0 > 0.0
504    }
505
506    #[inline]
507    fn is_negative(&self) -> bool {
508        self.0 < 0.0
509    }
510}
511
512impl ToPrimitive for Portable {
513    #[inline]
514    fn to_i64(&self) -> Option<i64> {
515        self.0.to_i64()
516    }
517
518    #[inline]
519    fn to_u64(&self) -> Option<u64> {
520        self.0.to_u64()
521    }
522
523    #[inline]
524    fn to_f64(&self) -> Option<f64> {
525        Some(self.0)
526    }
527
528    #[inline]
529    fn to_f32(&self) -> Option<f32> {
530        Some(self.0 as f32)
531    }
532}
533
534impl FromPrimitive for Portable {
535    #[inline]
536    fn from_i64(value: i64) -> Option<Self> {
537        Some(Self(value as f64))
538    }
539
540    #[inline]
541    fn from_u64(value: u64) -> Option<Self> {
542        Some(Self(value as f64))
543    }
544
545    #[inline]
546    fn from_f64(value: f64) -> Option<Self> {
547        Some(Self(value))
548    }
549
550    #[inline]
551    fn from_f32(value: f32) -> Option<Self> {
552        Some(Self(value as f64))
553    }
554}
555
556impl SubsetOf<Portable> for f64 {
557    #[inline]
558    fn to_superset(&self) -> Portable {
559        Portable(*self)
560    }
561
562    #[inline]
563    fn from_superset_unchecked(element: &Portable) -> Self {
564        element.0
565    }
566
567    #[inline]
568    fn is_in_subset(_: &Portable) -> bool {
569        true
570    }
571}
572
573impl SubsetOf<Portable> for f32 {
574    #[inline]
575    fn to_superset(&self) -> Portable {
576        Portable(f64::from(*self))
577    }
578
579    #[inline]
580    fn from_superset_unchecked(element: &Portable) -> Self {
581        element.0 as f32
582    }
583
584    #[inline]
585    fn is_in_subset(_: &Portable) -> bool {
586        true
587    }
588}
589
590impl SubsetOf<f64> for Portable {
591    #[inline]
592    fn to_superset(&self) -> f64 {
593        self.0
594    }
595
596    #[inline]
597    fn from_superset_unchecked(element: &f64) -> Self {
598        Self(*element)
599    }
600
601    #[inline]
602    fn is_in_subset(_: &f64) -> bool {
603        true
604    }
605}
606
607impl SubsetOf<f32> for Portable {
608    #[inline]
609    fn to_superset(&self) -> f32 {
610        self.0 as f32
611    }
612
613    #[inline]
614    fn from_superset_unchecked(element: &f32) -> Self {
615        Self(f64::from(*element))
616    }
617
618    #[inline]
619    fn is_in_subset(_: &f32) -> bool {
620        true
621    }
622}
623
624impl SubsetOf<Portable> for Portable {
625    #[inline]
626    fn to_superset(&self) -> Self {
627        *self
628    }
629
630    #[inline]
631    fn from_superset_unchecked(element: &Self) -> Self {
632        *element
633    }
634
635    #[inline]
636    fn is_in_subset(_: &Self) -> bool {
637        true
638    }
639}
640
641impl Field for Portable {}
642
643impl SimdValue for Portable {
644    const LANES: usize = 1;
645    type Element = Self;
646    type SimdBool = bool;
647
648    #[inline]
649    fn splat(value: Self::Element) -> Self {
650        value
651    }
652
653    #[inline]
654    fn extract(&self, _: usize) -> Self::Element {
655        *self
656    }
657
658    #[inline]
659    unsafe fn extract_unchecked(&self, _: usize) -> Self::Element {
660        *self
661    }
662
663    #[inline]
664    fn replace(&mut self, _: usize, value: Self::Element) {
665        *self = value;
666    }
667
668    #[inline]
669    unsafe fn replace_unchecked(&mut self, _: usize, value: Self::Element) {
670        *self = value;
671    }
672
673    #[inline]
674    fn select(self, condition: bool, other: Self) -> Self {
675        if condition {
676            self
677        } else {
678            other
679        }
680    }
681}
682
683impl PrimitiveSimdValue for Portable {}
684
685impl AbsDiffEq for Portable {
686    type Epsilon = Self;
687
688    #[inline]
689    fn default_epsilon() -> Self::Epsilon {
690        Self(f64::EPSILON)
691    }
692
693    #[inline]
694    fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
695        libm::fabs(self.0 - other.0) <= epsilon.0
696    }
697
698    #[inline]
699    fn abs_diff_ne(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
700        !self.abs_diff_eq(other, epsilon)
701    }
702}
703
704impl RelativeEq for Portable {
705    #[inline]
706    fn default_max_relative() -> Self::Epsilon {
707        Self(f64::EPSILON)
708    }
709
710    #[inline]
711    fn relative_eq(
712        &self,
713        other: &Self,
714        epsilon: Self::Epsilon,
715        max_relative: Self::Epsilon,
716    ) -> bool {
717        if self.abs_diff_eq(other, epsilon) {
718            true
719        } else {
720            libm::fabs(self.0 - other.0) <= max_relative.0 * self.0.abs().max(other.0.abs())
721        }
722    }
723
724    #[inline]
725    fn relative_ne(
726        &self,
727        other: &Self,
728        epsilon: Self::Epsilon,
729        max_relative: Self::Epsilon,
730    ) -> bool {
731        !self.relative_eq(other, epsilon, max_relative)
732    }
733}
734
735impl UlpsEq for Portable {
736    #[inline]
737    fn default_max_ulps() -> u32 {
738        4
739    }
740
741    #[inline]
742    fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool {
743        self.0.to_bits().abs_diff(other.0.to_bits()) <= u64::from(max_ulps)
744            || self.abs_diff_eq(other, epsilon)
745    }
746
747    #[inline]
748    fn ulps_ne(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool {
749        !self.ulps_eq(other, epsilon, max_ulps)
750    }
751}
752
753impl ComplexField for Portable {
754    type RealField = Self;
755
756    #[inline]
757    fn from_real(re: Self) -> Self {
758        re
759    }
760
761    #[inline]
762    fn real(self) -> Self {
763        self
764    }
765
766    #[inline]
767    fn imaginary(self) -> Self {
768        Self::zero()
769    }
770
771    #[inline]
772    fn norm1(self) -> Self {
773        self.abs()
774    }
775
776    #[inline]
777    fn modulus(self) -> Self {
778        self.abs()
779    }
780
781    #[inline]
782    fn modulus_squared(self) -> Self {
783        self * self
784    }
785
786    #[inline]
787    fn argument(self) -> Self {
788        if self >= Self::zero() {
789            Self::zero()
790        } else {
791            Self::pi()
792        }
793    }
794
795    #[inline]
796    fn to_exp(self) -> (Self, Self) {
797        if self >= Self::zero() {
798            (self, Self::one())
799        } else {
800            (-self, -Self::one())
801        }
802    }
803
804    #[inline]
805    fn recip(self) -> Self {
806        Self(self.0.recip())
807    }
808
809    #[inline]
810    fn conjugate(self) -> Self {
811        self
812    }
813
814    #[inline]
815    fn scale(self, factor: Self) -> Self {
816        self * factor
817    }
818
819    #[inline]
820    fn unscale(self, factor: Self) -> Self {
821        self / factor
822    }
823
824    #[inline]
825    fn floor(self) -> Self {
826        Self(libm::floor(self.0))
827    }
828
829    #[inline]
830    fn ceil(self) -> Self {
831        Self(libm::ceil(self.0))
832    }
833
834    #[inline]
835    fn round(self) -> Self {
836        Self(libm::round(self.0))
837    }
838
839    #[inline]
840    fn trunc(self) -> Self {
841        Self(libm::trunc(self.0))
842    }
843
844    #[inline]
845    fn fract(self) -> Self {
846        Self(self.0 - libm::trunc(self.0))
847    }
848
849    #[inline]
850    fn mul_add(self, a: Self, b: Self) -> Self {
851        Self(libm::fma(self.0, a.0, b.0))
852    }
853
854    #[inline]
855    fn abs(self) -> Self {
856        Self(libm::fabs(self.0))
857    }
858
859    #[inline]
860    fn hypot(self, other: Self) -> Self {
861        Self(libm::hypot(self.0, other.0))
862    }
863
864    #[inline]
865    fn powi(self, exponent: i32) -> Self {
866        if exponent == 0 {
867            return Self::one();
868        }
869        let negative = exponent < 0;
870        let mut power = exponent.unsigned_abs();
871        let mut base = self;
872        let mut result = Self::one();
873        while power != 0 {
874            if power & 1 != 0 {
875                result *= base;
876            }
877            base *= base;
878            power >>= 1;
879        }
880        if negative {
881            Self::one() / result
882        } else {
883            result
884        }
885    }
886
887    #[inline]
888    fn powf(self, exponent: Self) -> Self {
889        Self(libm::pow(self.0, exponent.0))
890    }
891
892    #[inline]
893    fn powc(self, exponent: Self) -> Self {
894        self.powf(exponent)
895    }
896
897    #[inline]
898    fn sqrt(self) -> Self {
899        Self(libm::sqrt(self.0))
900    }
901
902    #[inline]
903    fn try_sqrt(self) -> Option<Self> {
904        if self >= Self::zero() {
905            Some(self.sqrt())
906        } else {
907            None
908        }
909    }
910
911    #[inline]
912    fn exp(self) -> Self {
913        Self(libm::exp(self.0))
914    }
915
916    #[inline]
917    fn exp2(self) -> Self {
918        Self(libm::exp2(self.0))
919    }
920
921    #[inline]
922    fn exp_m1(self) -> Self {
923        Self(libm::expm1(self.0))
924    }
925
926    #[inline]
927    fn ln_1p(self) -> Self {
928        Self(libm::log1p(self.0))
929    }
930
931    #[inline]
932    fn ln(self) -> Self {
933        Self(libm::log(self.0))
934    }
935
936    #[inline]
937    fn log(self, base: Self) -> Self {
938        Self(libm::log(self.0) / libm::log(base.0))
939    }
940
941    #[inline]
942    fn log2(self) -> Self {
943        Self(libm::log2(self.0))
944    }
945
946    #[inline]
947    fn log10(self) -> Self {
948        Self(libm::log10(self.0))
949    }
950
951    #[inline]
952    fn cbrt(self) -> Self {
953        Self(libm::cbrt(self.0))
954    }
955
956    #[inline]
957    fn sin(self) -> Self {
958        Self(libm::sin(self.0))
959    }
960
961    #[inline]
962    fn cos(self) -> Self {
963        Self(libm::cos(self.0))
964    }
965
966    #[inline]
967    fn sin_cos(self) -> (Self, Self) {
968        let (sin, cos) = libm::sincos(self.0);
969        (Self(sin), Self(cos))
970    }
971
972    #[inline]
973    fn tan(self) -> Self {
974        Self(libm::tan(self.0))
975    }
976
977    #[inline]
978    fn asin(self) -> Self {
979        Self(libm::asin(self.0))
980    }
981
982    #[inline]
983    fn acos(self) -> Self {
984        Self(libm::acos(self.0))
985    }
986
987    #[inline]
988    fn atan(self) -> Self {
989        Self(libm::atan(self.0))
990    }
991
992    #[inline]
993    fn sinh(self) -> Self {
994        Self(libm::sinh(self.0))
995    }
996
997    #[inline]
998    fn cosh(self) -> Self {
999        Self(libm::cosh(self.0))
1000    }
1001
1002    #[inline]
1003    fn tanh(self) -> Self {
1004        Self(libm::tanh(self.0))
1005    }
1006
1007    #[inline]
1008    fn asinh(self) -> Self {
1009        Self(libm::asinh(self.0))
1010    }
1011
1012    #[inline]
1013    fn acosh(self) -> Self {
1014        Self(libm::acosh(self.0))
1015    }
1016
1017    #[inline]
1018    fn atanh(self) -> Self {
1019        Self(libm::atanh(self.0))
1020    }
1021
1022    #[inline]
1023    fn is_finite(&self) -> bool {
1024        self.0.is_finite()
1025    }
1026}
1027
1028impl RealField for Portable {
1029    #[inline]
1030    fn is_sign_positive(&self) -> bool {
1031        self.0.is_sign_positive()
1032    }
1033
1034    #[inline]
1035    fn is_sign_negative(&self) -> bool {
1036        self.0.is_sign_negative()
1037    }
1038
1039    #[inline]
1040    fn copysign(self, sign: Self) -> Self {
1041        Self(libm::copysign(self.0, sign.0))
1042    }
1043
1044    #[inline]
1045    fn max(self, other: Self) -> Self {
1046        Self(self.0.max(other.0))
1047    }
1048
1049    #[inline]
1050    fn min(self, other: Self) -> Self {
1051        Self(self.0.min(other.0))
1052    }
1053
1054    #[inline]
1055    fn clamp(self, min: Self, max: Self) -> Self {
1056        Self(self.0.clamp(min.0, max.0))
1057    }
1058
1059    #[inline]
1060    fn atan2(self, other: Self) -> Self {
1061        Self(libm::atan2(self.0, other.0))
1062    }
1063
1064    #[inline]
1065    fn min_value() -> Option<Self> {
1066        Some(Self(f64::MIN))
1067    }
1068
1069    #[inline]
1070    fn max_value() -> Option<Self> {
1071        Some(Self(f64::MAX))
1072    }
1073
1074    #[inline]
1075    fn pi() -> Self {
1076        Self(std::f64::consts::PI)
1077    }
1078
1079    #[inline]
1080    fn two_pi() -> Self {
1081        Self(std::f64::consts::PI + std::f64::consts::PI)
1082    }
1083
1084    #[inline]
1085    fn frac_pi_2() -> Self {
1086        Self(std::f64::consts::FRAC_PI_2)
1087    }
1088
1089    #[inline]
1090    fn frac_pi_3() -> Self {
1091        Self(std::f64::consts::FRAC_PI_3)
1092    }
1093
1094    #[inline]
1095    fn frac_pi_4() -> Self {
1096        Self(std::f64::consts::FRAC_PI_4)
1097    }
1098
1099    #[inline]
1100    fn frac_pi_6() -> Self {
1101        Self(std::f64::consts::FRAC_PI_6)
1102    }
1103
1104    #[inline]
1105    fn frac_pi_8() -> Self {
1106        Self(std::f64::consts::FRAC_PI_8)
1107    }
1108
1109    #[inline]
1110    fn frac_1_pi() -> Self {
1111        Self(std::f64::consts::FRAC_1_PI)
1112    }
1113
1114    #[inline]
1115    fn frac_2_pi() -> Self {
1116        Self(std::f64::consts::FRAC_2_PI)
1117    }
1118
1119    #[inline]
1120    fn frac_2_sqrt_pi() -> Self {
1121        Self(std::f64::consts::FRAC_2_SQRT_PI)
1122    }
1123
1124    #[inline]
1125    fn e() -> Self {
1126        Self(std::f64::consts::E)
1127    }
1128
1129    #[inline]
1130    fn log2_e() -> Self {
1131        Self(std::f64::consts::LOG2_E)
1132    }
1133
1134    #[inline]
1135    fn log10_e() -> Self {
1136        Self(std::f64::consts::LOG10_E)
1137    }
1138
1139    #[inline]
1140    fn ln_2() -> Self {
1141        Self(std::f64::consts::LN_2)
1142    }
1143
1144    #[inline]
1145    fn ln_10() -> Self {
1146        Self(std::f64::consts::LN_10)
1147    }
1148}
1149
1150impl FromStr for Portable {
1151    type Err = ParseFloatError;
1152
1153    #[inline]
1154    fn from_str(value: &str) -> Result<Self, Self::Err> {
1155        value.parse().map(Self)
1156    }
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161    use super::*;
1162    use std::cell::RefCell;
1163
1164    #[derive(Default)]
1165    struct LossHookProbe {
1166        log1p_inputs: RefCell<Vec<f64>>,
1167        atan_inputs: RefCell<Vec<f64>>,
1168    }
1169
1170    impl HostNumerics for LossHookProbe {
1171        fn svd(
1172            &self,
1173            values: &[f64],
1174            rows: usize,
1175            cols: usize,
1176        ) -> Result<(Vec<f64>, Vec<f64>, Vec<f64>), BackendError> {
1177            PortableNumerics.svd(values, rows, cols)
1178        }
1179
1180        fn log1p(&self, value: f64) -> Result<Option<f64>, BackendError> {
1181            self.log1p_inputs.borrow_mut().push(value);
1182            Ok(Some(17.0 + value))
1183        }
1184
1185        fn atan(&self, value: f64) -> Result<Option<f64>, BackendError> {
1186            self.atan_inputs.borrow_mut().push(value);
1187            Ok(Some(23.0 + value))
1188        }
1189    }
1190
1191    fn samples(count: usize) -> Vec<f64> {
1192        let mut state = 0x9e3779b97f4a7c15_u64;
1193        (0..count)
1194            .map(|_| {
1195                state = state
1196                    .wrapping_mul(0xd1342543de82ef95)
1197                    .wrapping_add(0xa4093822299f31d0);
1198                let fraction = f64::from_bits(0x3ff0000000000000 | (state >> 12)) - 1.0;
1199                let signed = if state & 1 == 0 { fraction } else { -fraction };
1200                signed * 64.0
1201            })
1202            .collect()
1203    }
1204
1205    fn repeated_square(value: f64, exponent: i32) -> f64 {
1206        let negative = exponent < 0;
1207        let mut power = exponent.unsigned_abs();
1208        let mut base = value;
1209        let mut result = 1.0;
1210        while power != 0 {
1211            if power & 1 != 0 {
1212                result *= base;
1213            }
1214            base *= base;
1215            power >>= 1;
1216        }
1217        if negative {
1218            1.0 / result
1219        } else {
1220            result
1221        }
1222    }
1223
1224    #[test]
1225    fn arithmetic_is_a_bit_copy_of_binary64() {
1226        let mut values = samples(128);
1227        values.extend([0.0, -0.0, 1.25, -3.5, f64::MIN_POSITIVE, f64::MAX]);
1228        for &left in &values {
1229            for &right in &values {
1230                assert_eq!(
1231                    (Portable(left) + Portable(right)).0.to_bits(),
1232                    (left + right).to_bits()
1233                );
1234                assert_eq!(
1235                    (Portable(left) - Portable(right)).0.to_bits(),
1236                    (left - right).to_bits()
1237                );
1238                assert_eq!(
1239                    (Portable(left) * Portable(right)).0.to_bits(),
1240                    (left * right).to_bits()
1241                );
1242                assert_eq!(
1243                    (Portable(left) / Portable(right)).0.to_bits(),
1244                    (left / right).to_bits()
1245                );
1246                assert_eq!(
1247                    Portable(left).partial_cmp(&Portable(right)),
1248                    left.partial_cmp(&right)
1249                );
1250                assert_eq!(Portable(left) < Portable(right), left < right);
1251                assert_eq!(Portable(left) <= Portable(right), left <= right);
1252                assert_eq!(Portable(left) > Portable(right), left > right);
1253                assert_eq!(Portable(left) >= Portable(right), left >= right);
1254            }
1255        }
1256    }
1257
1258    #[test]
1259    fn transcendental_delegates_match_libm() {
1260        for value in samples(128) {
1261            let portable = Portable(value);
1262            assert_eq!(portable.sin().0.to_bits(), libm::sin(value).to_bits());
1263            assert_eq!(portable.cos().0.to_bits(), libm::cos(value).to_bits());
1264            let (sin, cos) = portable.sin_cos();
1265            let (expected_sin, expected_cos) = libm::sincos(value);
1266            assert_eq!(sin.0.to_bits(), expected_sin.to_bits());
1267            assert_eq!(cos.0.to_bits(), expected_cos.to_bits());
1268            assert_eq!(portable.tan().0.to_bits(), libm::tan(value).to_bits());
1269            assert_eq!(portable.sinh().0.to_bits(), libm::sinh(value).to_bits());
1270            assert_eq!(portable.cosh().0.to_bits(), libm::cosh(value).to_bits());
1271            assert_eq!(portable.tanh().0.to_bits(), libm::tanh(value).to_bits());
1272            assert_eq!(portable.asinh().0.to_bits(), libm::asinh(value).to_bits());
1273            assert_eq!(portable.cbrt().0.to_bits(), libm::cbrt(value).to_bits());
1274            assert_eq!(
1275                portable.hypot(portable).0.to_bits(),
1276                libm::hypot(value, value).to_bits()
1277            );
1278            assert_eq!(portable.sqrt().0.to_bits(), libm::sqrt(value).to_bits());
1279            assert_eq!(portable.exp().0.to_bits(), libm::exp(value).to_bits());
1280            assert_eq!(portable.exp2().0.to_bits(), libm::exp2(value).to_bits());
1281            assert_eq!(portable.exp_m1().0.to_bits(), libm::expm1(value).to_bits());
1282            assert_eq!(portable.atan().0.to_bits(), libm::atan(value).to_bits());
1283            assert_eq!(
1284                portable.atan2(portable).0.to_bits(),
1285                libm::atan2(value, value).to_bits()
1286            );
1287            let positive = value.abs() + 0.25;
1288            assert_eq!(
1289                Portable(positive).ln().0.to_bits(),
1290                libm::log(positive).to_bits()
1291            );
1292            assert_eq!(
1293                Portable(positive).log2().0.to_bits(),
1294                libm::log2(positive).to_bits()
1295            );
1296            assert_eq!(
1297                Portable(positive).log10().0.to_bits(),
1298                libm::log10(positive).to_bits()
1299            );
1300            assert_eq!(
1301                Portable(value / 64.0).asin().0.to_bits(),
1302                libm::asin(value / 64.0).to_bits()
1303            );
1304            assert_eq!(
1305                Portable(value / 64.0).acos().0.to_bits(),
1306                libm::acos(value / 64.0).to_bits()
1307            );
1308            assert_eq!(
1309                Portable(value / 64.0).atanh().0.to_bits(),
1310                libm::atanh(value / 64.0).to_bits()
1311            );
1312            assert_eq!(
1313                Portable(value / 64.0 + 0.5).ln_1p().0.to_bits(),
1314                libm::log1p(value / 64.0 + 0.5).to_bits()
1315            );
1316            assert_eq!(
1317                Portable(value / 64.0 + 1.0).acosh().0.to_bits(),
1318                libm::acosh(value / 64.0 + 1.0).to_bits()
1319            );
1320            let exponent = (value as i32) % 8;
1321            assert_eq!(
1322                Portable(value).powi(exponent).0.to_bits(),
1323                repeated_square(value, exponent).to_bits()
1324            );
1325            assert_eq!(
1326                Portable(positive).powf(Portable(value / 8.0)).0.to_bits(),
1327                libm::pow(positive, value / 8.0).to_bits()
1328            );
1329            assert_eq!(
1330                Portable(positive).log(Portable(positive + 0.5)).0.to_bits(),
1331                (libm::log(positive) / libm::log(positive + 0.5)).to_bits()
1332            );
1333        }
1334    }
1335
1336    #[test]
1337    fn portable_loss_backend_hooks_are_consulted() {
1338        use trust_region_least_squares::loss::{Loss, LossFunction};
1339
1340        let probe = LossHookProbe::default();
1341        let cauchy = LossFunction::new(Loss::Cauchy, 1.0)
1342            .evaluate_with(&[0.5], &probe)
1343            .expect("Cauchy hook");
1344        assert_eq!(probe.log1p_inputs.borrow().as_slice(), &[0.25]);
1345        assert_eq!(cauchy.rho0, vec![17.25]);
1346
1347        let arctan = LossFunction::new(Loss::Arctan, 1.0)
1348            .evaluate_with(&[0.5], &probe)
1349            .expect("Arctan hook");
1350        assert_eq!(probe.atan_inputs.borrow().as_slice(), &[0.25]);
1351        assert_eq!(arctan.rho0, vec![23.25]);
1352
1353        assert_eq!(
1354            PortableNumerics
1355                .log1p(0.25)
1356                .expect("portable log1p")
1357                .expect("supplied log1p")
1358                .to_bits(),
1359            libm::log1p(0.25).to_bits()
1360        );
1361        assert_eq!(
1362            PortableNumerics
1363                .atan(0.25)
1364                .expect("portable atan")
1365                .expect("supplied atan")
1366                .to_bits(),
1367            libm::atan(0.25).to_bits()
1368        );
1369    }
1370
1371    #[test]
1372    fn portable_fma_matches_libm() {
1373        let values = samples(128);
1374        for (index, &left) in values.iter().enumerate() {
1375            let right = values[(index * 37) % values.len()];
1376            let addend = values[(index * 71 + 11) % values.len()];
1377            assert_eq!(
1378                Portable(left)
1379                    .mul_add(Portable(right), Portable(addend))
1380                    .0
1381                    .to_bits(),
1382                libm::fma(left, right, addend).to_bits()
1383            );
1384        }
1385    }
1386}