Skip to main content

torsh_tensor/
math_ops_trig.rs

1//! Transcendental and trigonometric math operations for tensors.
2//!
3//! This module is included by `math_ops.rs` via `#[path]` and re-exported with `pub use`.
4//! It covers all floating-point transcendental functions:
5//! - Square root, exponential, logarithms
6//! - Trigonometric: sin, cos, tan, asin, acos, atan
7//! - Hyperbolic: sinh, cosh, tanh
8//! - Activation: GELU, leaky ReLU
9//! - Rounding: floor, ceil, round, trunc, fract
10//! - Power, negation, sign
11
12use super::*;
13
14// Mathematical functions for floating-point tensors
15impl<T: TensorElement + Copy> Tensor<T>
16where
17    T: scirs2_core::numeric::Float + torsh_core::dtype::FloatElement,
18{
19    /// Square root of all elements
20    pub fn sqrt(&self) -> Result<Self> {
21        let result = self.map(|x| x.sqrt())?;
22        Ok(self.record_unary(result, UnaryKind::Sqrt))
23    }
24
25    /// Square of all elements
26    ///
27    /// Records [`Operation::Power`] with exponent `2.0`: `map` is
28    /// forward-only (see [`Tensor::neg`]'s doc for why), so without this
29    /// `square()` would return a `requires_grad` leaf that swallows the
30    /// gradient of everything upstream of it. The `Power` backward arm
31    /// already exists and needs no extra trait bound here -- `2.0` is a
32    /// literal `f32`, not a value converted from `T` (that conversion is
33    /// what [`Tensor::pow`]'s `Into<f32>` bound is for).
34    pub fn square(&self) -> Result<Self> {
35        let mut result = self.map(|x| x * x)?;
36        if crate::should_record_grad(self.requires_grad) {
37            result.requires_grad = true;
38            result.operation = Operation::Power {
39                input: Arc::new(self.clone()),
40                exponent: 2.0,
41            };
42        }
43        Ok(result)
44    }
45
46    /// Reciprocal square root of all elements (1/sqrt(x))
47    pub fn rsqrt(&self) -> Result<Self> {
48        let result =
49            self.map(|x| T::from(1.0).expect("numeric conversion should succeed") / x.sqrt())?;
50        Ok(self.record_unary(result, UnaryKind::Rsqrt))
51    }
52
53    /// Reciprocal of all elements (1/x)
54    pub fn reciprocal(&self) -> Result<Self> {
55        let result = self.map(|x| T::from(1.0).expect("numeric conversion should succeed") / x)?;
56        Ok(self.record_unary(result, UnaryKind::Reciprocal))
57    }
58
59    /// Exponential of all elements
60    pub fn exp(&self) -> Result<Self> {
61        let result = self.map(|x| x.exp())?;
62        Ok(self.record_unary(result, UnaryKind::Exp))
63    }
64
65    /// Natural logarithm of all elements
66    pub fn ln(&self) -> Result<Self> {
67        let result = self.map(|x| x.ln())?;
68        Ok(self.record_unary(result, UnaryKind::Ln))
69    }
70
71    /// Logarithm base 10 of all elements
72    pub fn log10(&self) -> Result<Self> {
73        let result = self.map(|x| x.log10())?;
74        Ok(self.record_unary(result, UnaryKind::Log10))
75    }
76
77    /// Logarithm base 2 of all elements
78    pub fn log2(&self) -> Result<Self> {
79        let result = self.map(|x| x.log2())?;
80        Ok(self.record_unary(result, UnaryKind::Log2))
81    }
82
83    /// Natural logarithm of all elements
84    pub fn log(&self) -> Result<Self> {
85        let result = self.map(|x| x.ln())?;
86        Ok(self.record_unary(result, UnaryKind::Ln))
87    }
88
89    /// Sine of all elements
90    pub fn sin(&self) -> Result<Self> {
91        let result = self.map(|x| x.sin())?;
92        Ok(self.record_unary(result, UnaryKind::Sin))
93    }
94
95    /// Cosine of all elements
96    pub fn cos(&self) -> Result<Self> {
97        let result = self.map(|x| x.cos())?;
98        Ok(self.record_unary(result, UnaryKind::Cos))
99    }
100
101    /// Tangent of all elements
102    pub fn tan(&self) -> Result<Self> {
103        let result = self.map(|x| x.tan())?;
104        Ok(self.record_unary(result, UnaryKind::Tan))
105    }
106
107    /// GELU (Gaussian Error Linear Unit) activation, in the tanh
108    /// approximation `0.5*x*(1 + tanh(sqrt(2/pi)*(x + 0.044715*x^3)))`.
109    ///
110    /// One closed form at every tensor size — see the private `gelu_forward`
111    /// below for why the f32 SIMD kernel is deliberately not used — and one recorded
112    /// derivative ([`crate::core_ops::UnaryKind::Gelu`]) that matches it exactly.
113    pub fn gelu(&self) -> Result<Self> {
114        let result = self.gelu_forward()?;
115        Ok(self.record_unary(result, UnaryKind::Gelu))
116    }
117
118    /// Forward-only GELU (parallel or scalar path); autograd is recorded by
119    /// the public [`Tensor::gelu`] wrapper, so every dispatch path shares one
120    /// recorded derivative.
121    ///
122    /// **There is deliberately no f32 SIMD path.** Until Wave 4, tensors with
123    /// `numel > 1000` were routed to scirs2-core's `adaptive_simd_gelu_f32`,
124    /// which substitutes a clamped Pade rational for `tanh`. That is a
125    /// *different function*, not a faster evaluation of the same one: the
126    /// forward jumped by up to 0.0211 absolute (515% relative at `x = -2.40`)
127    /// the moment a tensor crossed 1000 elements, and the recorded backward —
128    /// which differentiates the exact-tanh closed form — no longer matched the
129    /// forward that had run. Both defects are pinned by
130    /// `tests/hardening_autograd_primitives.rs`
131    /// (`gelu_forward_is_continuous_across_the_dispatch_threshold` and
132    /// `gelu_backward_matches_finite_difference_above_the_simd_threshold`).
133    /// The parallel path below still fans large tensors out across worker
134    /// threads; it just evaluates the same `compute_gelu_scalar` everywhere.
135    fn gelu_forward(&self) -> Result<Self> {
136        // GPU fast path (deferred): GELU is not yet a variant of oxicuda's
137        // `ComputeBackend` `UnaryOp`.  oxicuda-blas already ships
138        // `elementwise::unary::gelu`, so once `UnaryOp::Gelu` is added upstream
139        // (TODO(oxicuda-unaryop-gelu)) dispatch here via
140        // `crate::gpu_dispatch::try_unary_f32(self, UnaryOp::Gelu)`.
141
142        // ✅ SciRS2 Parallel Processing - Use parallel computation for medium
143        // and large tensors (there is no separate SIMD band; see above).
144        #[cfg(feature = "parallel")]
145        {
146            if self.numel() > 100 {
147                return self.parallel_map(|x| self.compute_gelu_scalar(x));
148            }
149        }
150
151        // Fallback to sequential processing
152        // GELU(x) = 0.5 * x * (1 + tanh(sqrt(2/π) * (x + 0.044715 * x^3)))
153        self.map(|x| self.compute_gelu_scalar(x))
154    }
155
156    /// Compute GELU for a single scalar value
157    fn compute_gelu_scalar(&self, x: T) -> T {
158        let pi = T::from(std::f64::consts::PI).expect("numeric conversion should succeed");
159        let two = T::from(2.0).expect("numeric conversion should succeed");
160        let sqrt_2_over_pi = (two / pi).sqrt();
161        let point_044715 = T::from(0.044715).expect("numeric conversion should succeed");
162        let one = <T as scirs2_core::numeric::One>::one();
163        let half = T::from(0.5).expect("numeric conversion should succeed");
164
165        let x_cubed = x * x * x;
166        let tanh_input = sqrt_2_over_pi * (x + point_044715 * x_cubed);
167        half * x * (one + tanh_input.tanh())
168    }
169
170    /// Leaky ReLU activation function with negative slope
171    pub fn leaky_relu(&self, negative_slope: T) -> Result<Self> {
172        let result = self.leaky_relu_forward(negative_slope)?;
173        Ok(self.record_leaky_relu(result, negative_slope))
174    }
175
176    /// Forward-only leaky ReLU; autograd is recorded by the public
177    /// [`Tensor::leaky_relu`] wrapper.
178    fn leaky_relu_forward(&self, negative_slope: T) -> Result<Self> {
179        // GPU fast path (deferred): parameterized LeakyReLU has no oxicuda
180        // `ComputeBackend` `UnaryOp` variant (the flat `unary` op takes no
181        // scalar parameter).  TODO(oxicuda-unaryop-leakyrelu): add a
182        // parameterized variant upstream, then dispatch via gpu_dispatch.
183        self.map(|x| {
184            if x > scirs2_core::numeric::Zero::zero() {
185                x
186            } else {
187                negative_slope * x
188            }
189        })
190    }
191
192    /// Arcsine of all elements
193    pub fn asin(&self) -> Result<Self> {
194        let result = self.map(|x| x.asin())?;
195        Ok(self.record_unary(result, UnaryKind::Asin))
196    }
197
198    /// Arccosine of all elements
199    pub fn acos(&self) -> Result<Self> {
200        let result = self.map(|x| x.acos())?;
201        Ok(self.record_unary(result, UnaryKind::Acos))
202    }
203
204    /// Arctangent of all elements
205    pub fn atan(&self) -> Result<Self> {
206        let result = self.map(|x| x.atan())?;
207        Ok(self.record_unary(result, UnaryKind::Atan))
208    }
209
210    /// Hyperbolic sine of all elements
211    pub fn sinh(&self) -> Result<Self> {
212        let result = self.map(|x| x.sinh())?;
213        Ok(self.record_unary(result, UnaryKind::Sinh))
214    }
215
216    /// Hyperbolic cosine of all elements
217    pub fn cosh(&self) -> Result<Self> {
218        let result = self.map(|x| x.cosh())?;
219        Ok(self.record_unary(result, UnaryKind::Cosh))
220    }
221
222    /// Hyperbolic tangent of all elements
223    pub fn tanh(&self) -> Result<Self> {
224        let result = self.tanh_forward()?;
225        Ok(self.record_unary(result, UnaryKind::Tanh))
226    }
227
228    /// Forward-only tanh (GPU or scalar path); autograd is recorded by the
229    /// public [`Tensor::tanh`] wrapper.
230    fn tanh_forward(&self) -> Result<Self> {
231        // GPU fast path: f32 CUDA tensors dispatch to oxicuda's ComputeBackend.
232        // Declines to None (CPU fallback) unless a GPU backend is active.
233        #[cfg(feature = "gpu")]
234        if let Some(result) =
235            crate::gpu_dispatch::try_unary_f32(self, crate::gpu_dispatch::UnaryOp::Tanh)
236        {
237            return Ok(result);
238        }
239
240        self.map(|x| x.tanh())
241    }
242
243    /// Power function (element-wise)
244    pub fn pow(&self, exponent: T) -> Result<Self>
245    where
246        T: TensorElement + Into<f32>,
247    {
248        // Convert T to f32 for the Operation::Power storage
249        let exponent_f32: f32 = exponent.into();
250
251        let mut result = self.map(|x| x.powf(exponent))?;
252
253        // Set up gradient computation if needed
254        if crate::should_record_grad(self.requires_grad) {
255            result.requires_grad = true;
256            result.operation = Operation::Power {
257                input: Arc::new(self.clone()),
258                exponent: exponent_f32,
259            };
260        }
261
262        Ok(result)
263    }
264
265    /// Power function with scalar exponent (alias for pow)
266    pub fn pow_scalar(&self, exponent: T) -> Result<Self>
267    where
268        T: TensorElement + Into<f32>,
269    {
270        self.pow(exponent)
271    }
272
273    /// Power function with tensor exponents
274    pub fn pow_tensor(&self, exponent: &Self) -> Result<Self> {
275        self.elementwise_operation(exponent, |base, exp| base.powf(exp))
276    }
277
278    /// Floor of all elements
279    ///
280    /// a.e. zero derivative: the result is detached.
281    pub fn floor(&self) -> Result<Self> {
282        self.map(|x| x.floor())
283    }
284
285    /// Ceiling of all elements
286    ///
287    /// a.e. zero derivative: the result is detached.
288    pub fn ceil(&self) -> Result<Self> {
289        self.map(|x| x.ceil())
290    }
291
292    /// Round to nearest integer
293    ///
294    /// a.e. zero derivative: the result is detached.
295    pub fn round(&self) -> Result<Self> {
296        self.map(|x| x.round())
297    }
298
299    /// Truncate to integer part
300    ///
301    /// a.e. zero derivative: the result is detached.
302    pub fn trunc(&self) -> Result<Self> {
303        self.map(|x| x.trunc())
304    }
305
306    /// Fractional part
307    ///
308    /// a.e. zero derivative (`fract` is `x - trunc(x)`, and the recorded
309    /// pass-through would be wrong at every integer): the result is detached.
310    pub fn fract(&self) -> Result<Self> {
311        self.map(|x| x.fract())
312    }
313
314    /// Negation of all elements
315    ///
316    /// Records [`Operation::MulScalar`] with `-1`: `map` is forward-only, so
317    /// without this the negated tensor would be a `requires_grad` leaf that
318    /// swallows the gradient of everything upstream of it.
319    pub fn neg(&self) -> Result<Self>
320    where
321        T: std::ops::Neg<Output = T>,
322    {
323        let mut result = self.map(|x| -x)?;
324        if crate::should_record_grad(self.requires_grad) {
325            result.requires_grad = true;
326            result.operation = Operation::MulScalar {
327                input: Arc::new(self.clone()),
328                scalar: -<T as TensorElement>::one(),
329            };
330        }
331        Ok(result)
332    }
333
334    /// Sign of all elements (-1, 0, or 1)
335    ///
336    /// a.e. zero derivative: the result is detached.
337    pub fn sign(&self) -> Result<Self> {
338        self.map(|x| {
339            if x > <T as scirs2_core::numeric::Zero>::zero() {
340                <T as scirs2_core::numeric::One>::one()
341            } else if x < <T as scirs2_core::numeric::Zero>::zero() {
342                -<T as scirs2_core::numeric::One>::one()
343            } else {
344                <T as scirs2_core::numeric::Zero>::zero()
345            }
346        })
347    }
348}