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