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