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}