Skip to main content

torsh_tensor/
convenience.rs

1//! Convenience methods for tensor manipulation
2//!
3//! This module provides convenient shortcuts and aliases for common tensor operations
4//! to improve ergonomics and match PyTorch/NumPy APIs.
5
6use crate::{Tensor, TensorElement};
7use torsh_core::error::Result;
8
9/// Convenience trait for tensor manipulation shortcuts
10pub trait TensorConvenience<T: TensorElement> {
11    /// Transpose shortcut (equivalent to .transpose())
12    ///
13    /// # Examples
14    /// ```
15    /// # use torsh_tensor::{tensor_2d, convenience::TensorConvenience};
16    /// let tensor = tensor_2d!([&[1.0, 2.0], &[3.0, 4.0]]).expect("tensor creation failed");
17    /// let transposed = tensor.T().expect("transpose failed");
18    /// ```
19    #[allow(non_snake_case)]
20    fn T(&self) -> Result<Tensor<T>>;
21
22    /// Matrix transpose (alias for .T())
23    #[allow(non_snake_case)]
24    fn mT(&self) -> Result<Tensor<T>>;
25
26    /// Hermitian transpose (conjugate transpose for complex numbers)
27    #[allow(non_snake_case)]
28    fn H(&self) -> Result<Tensor<T>>;
29
30    /// Transpose shortcut (snake_case version)
31    fn t(&self) -> Result<Tensor<T>>;
32
33    /// Matrix transpose (snake_case version)
34    fn m_t(&self) -> Result<Tensor<T>>;
35
36    /// Hermitian transpose (snake_case version)
37    fn h(&self) -> Result<Tensor<T>>;
38
39    /// Detach tensor from computational graph (creates a new tensor without gradients)
40    fn detach(&self) -> Tensor<T>;
41
42    /// Clone tensor data (creates a deep copy)
43    fn clone_tensor(&self) -> Result<Tensor<T>>;
44
45    /// Check if tensor is contiguous in memory
46    fn is_contiguous(&self) -> bool;
47
48    /// Make tensor contiguous (reorganize memory layout)
49    fn contiguous(&self) -> Result<Tensor<T>>;
50
51    /// Get number of elements in tensor
52    fn numel(&self) -> usize;
53
54    /// Get tensor size (alias for shape().dims())
55    fn size(&self) -> Vec<usize>;
56
57    /// Check if tensor is empty (has zero elements)
58    fn is_empty(&self) -> bool;
59
60    /// Check if tensor is scalar (zero dimensions)
61    fn is_scalar(&self) -> bool;
62
63    /// Get tensor item as scalar (only works for scalar tensors)
64    fn item(&self) -> T;
65
66    /// Convert tensor to scalar (squeezes all dimensions of size 1 first)
67    fn to_scalar(&self) -> Result<T>;
68}
69
70impl<T: TensorElement + Copy + torsh_core::FloatElement> TensorConvenience<T> for Tensor<T> {
71    #[allow(non_snake_case)]
72    fn T(&self) -> Result<Tensor<T>> {
73        // For 2D tensors, transpose is straightforward
74        if self.shape().dims().len() == 2 {
75            self.transpose(0, 1)
76        } else if self.shape().dims().len() == 1 {
77            // 1D tensor transpose returns the same tensor
78            Ok(self.clone())
79        } else {
80            // For higher dimensional tensors, transpose last two dimensions
81            let ndim = self.shape().dims().len();
82            if ndim >= 2 {
83                self.transpose((ndim - 2) as i32, (ndim - 1) as i32)
84            } else {
85                Ok(self.clone())
86            }
87        }
88    }
89
90    #[allow(non_snake_case)]
91    fn mT(&self) -> Result<Tensor<T>> {
92        self.T()
93    }
94
95    #[allow(non_snake_case)]
96    fn H(&self) -> Result<Tensor<T>> {
97        // For real numbers, Hermitian transpose is just transpose
98        // For complex numbers, we need conjugate transpose
99        let transposed = self.T()?;
100
101        // If T implements conjugate operation, apply it
102        // For now, just return transpose for real numbers
103        Ok(transposed)
104    }
105
106    fn t(&self) -> Result<Tensor<T>> {
107        self.T()
108    }
109
110    fn m_t(&self) -> Result<Tensor<T>> {
111        self.T()
112    }
113
114    fn h(&self) -> Result<Tensor<T>> {
115        self.H()
116    }
117
118    fn detach(&self) -> Tensor<T> {
119        // Create a new tensor without gradient tracking
120        // For now, just return a clone since we don't have gradient tracking implemented
121        self.clone()
122    }
123
124    fn clone_tensor(&self) -> Result<Tensor<T>> {
125        Ok(self.detach())
126    }
127
128    fn is_contiguous(&self) -> bool {
129        let shape_ref = self.shape();
130        if shape_ref.dims().is_empty() {
131            return true; // scalar is always contiguous
132        }
133        // None means default strides → always contiguous by construction
134        match &self.strides {
135            None => true,
136            Some(strides) => {
137                let expected = self.compute_default_strides();
138                strides == &expected
139            }
140        }
141    }
142
143    fn contiguous(&self) -> Result<Tensor<T>> {
144        if self.is_contiguous() {
145            Ok(self.clone())
146        } else {
147            // Reorganize memory layout to be contiguous
148            self.clone_tensor()
149        }
150    }
151
152    fn numel(&self) -> usize {
153        self.shape().dims().iter().product()
154    }
155
156    fn size(&self) -> Vec<usize> {
157        self.shape().dims().to_vec()
158    }
159
160    fn is_empty(&self) -> bool {
161        self.numel() == 0
162    }
163
164    fn is_scalar(&self) -> bool {
165        self.shape().dims().is_empty()
166    }
167
168    fn item(&self) -> T {
169        // Get a single item from scalar tensor
170        if self.numel() != 1 {
171            panic!("Can only call item() on tensors with one element");
172        }
173        let data = self
174            .to_vec()
175            .expect("tensor to vec conversion should succeed");
176        data[0]
177    }
178
179    fn to_scalar(&self) -> Result<T> {
180        // First squeeze all dimensions of size 1
181        let squeezed = self.squeeze_all()?;
182        squeezed.item()
183    }
184}
185
186/// Additional convenience methods for specific tensor operations
187pub trait TensorShapeConvenience<T: TensorElement> {
188    /// Add singleton dimension at specified position
189    fn unsqueeze_at(&self, dim: i32) -> Result<Tensor<T>>;
190
191    /// Remove all singleton dimensions
192    fn squeeze_all(&self) -> Result<Tensor<T>>;
193
194    /// Flatten tensor to 1D (preserving total number of elements)
195    fn flatten(&self) -> Result<Tensor<T>>;
196
197    /// Flatten tensor starting from specified dimension
198    fn flatten_from(&self, start_dim: i32) -> Result<Tensor<T>>;
199
200    /// Unflatten tensor back to specified shape
201    fn unflatten(&self, dim: i32, sizes: &[usize]) -> Result<Tensor<T>>;
202}
203
204impl<T: TensorElement + Copy> TensorShapeConvenience<T> for Tensor<T> {
205    fn unsqueeze_at(&self, dim: i32) -> Result<Tensor<T>> {
206        self.unsqueeze(dim)
207    }
208
209    fn squeeze_all(&self) -> Result<Tensor<T>> {
210        let mut result = self.clone();
211        let shape_ref = self.shape();
212        let dims = shape_ref.dims();
213
214        // Remove all dimensions of size 1
215        for (i, &size) in dims.iter().enumerate().rev() {
216            if size == 1 {
217                result = result.squeeze(i as i32)?;
218            }
219        }
220
221        Ok(result)
222    }
223
224    fn flatten(&self) -> Result<Tensor<T>> {
225        let total_elements = self.numel();
226        self.reshape(&[total_elements as i32])
227    }
228
229    fn flatten_from(&self, start_dim: i32) -> Result<Tensor<T>> {
230        let shape_ref = self.shape();
231        let shape = shape_ref.dims();
232        let ndim = shape.len() as i32;
233        let start_dim = if start_dim < 0 {
234            ndim + start_dim
235        } else {
236            start_dim
237        };
238
239        if start_dim < 0 || start_dim >= ndim {
240            return Err(torsh_core::error::TorshError::InvalidArgument(format!(
241                "Invalid start_dim {start_dim} for tensor with {ndim} dimensions"
242            )));
243        }
244
245        let mut new_shape = Vec::new();
246
247        // Keep dimensions before start_dim
248        for &dim in shape.iter().take(start_dim as usize) {
249            new_shape.push(dim);
250        }
251
252        // Flatten dimensions from start_dim onwards
253        let flattened_size: usize = shape[start_dim as usize..].iter().product();
254        new_shape.push(flattened_size);
255
256        let new_shape_i32: Vec<i32> = new_shape.iter().map(|&x| x as i32).collect();
257        self.reshape(&new_shape_i32)
258    }
259
260    fn unflatten(&self, dim: i32, sizes: &[usize]) -> Result<Tensor<T>> {
261        let shape_ref = self.shape();
262        let shape = shape_ref.dims();
263        let ndim = shape.len() as i32;
264        let dim = if dim < 0 { ndim + dim } else { dim };
265
266        if dim < 0 || dim >= ndim {
267            return Err(torsh_core::error::TorshError::InvalidArgument(format!(
268                "Invalid dim {dim} for tensor with {ndim} dimensions"
269            )));
270        }
271
272        // Check that sizes product matches the dimension size
273        let expected_size = shape[dim as usize];
274        let actual_size: usize = sizes.iter().product();
275
276        if expected_size != actual_size {
277            return Err(torsh_core::error::TorshError::InvalidArgument(format!(
278                "Sizes {actual_size} don't multiply to dimension size {expected_size}"
279            )));
280        }
281
282        // Build new shape
283        let mut new_shape = Vec::new();
284
285        // Add dimensions before the target dimension
286        for &dim_size in shape.iter().take(dim as usize) {
287            new_shape.push(dim_size);
288        }
289
290        // Add the unflattened dimensions
291        new_shape.extend_from_slice(sizes);
292
293        // Add dimensions after the target dimension
294        for &dim_size in shape.iter().skip(dim as usize + 1) {
295            new_shape.push(dim_size);
296        }
297
298        let new_shape_i32: Vec<i32> = new_shape.iter().map(|&x| x as i32).collect();
299        self.reshape(&new_shape_i32)
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_transpose_shortcuts() {
309        let tensor = crate::creation::tensor_2d_arrays(&[[1.0f32, 2.0], [3.0, 4.0]])
310            .expect("tensor creation failed");
311
312        // Test .T() shortcut
313        let transposed = tensor.T().expect("T() failed");
314        assert_eq!(transposed.shape().dims(), &[2, 2]);
315
316        // Test .mT() alias
317        let mt_transposed = tensor.mT().expect("mT() failed");
318        assert_eq!(mt_transposed.shape().dims(), &[2, 2]);
319
320        // Test .H() (should be same as .T() for real numbers)
321        let hermitian = tensor.H().expect("H() failed");
322        assert_eq!(hermitian.shape().dims(), &[2, 2]);
323    }
324
325    #[test]
326    fn test_tensor_properties() {
327        let tensor = crate::creation::tensor_2d_arrays(&[[1.0f32, 2.0], [3.0, 4.0]])
328            .expect("tensor creation failed");
329
330        assert_eq!(tensor.numel(), 4);
331        assert_eq!(tensor.shape().dims(), &[2, 2]);
332        assert!(!tensor.is_empty());
333        assert!(!tensor.is_scalar());
334        assert!(tensor.is_contiguous());
335
336        // Test scalar tensor
337        let scalar = crate::creation::tensor_scalar(42.0f32).expect("scalar creation failed");
338        assert!(scalar.is_scalar());
339        assert_eq!(scalar.item().expect("item retrieval failed"), 42.0);
340    }
341
342    #[test]
343    fn test_shape_convenience() {
344        // Create a 3D tensor with shape [2, 1, 2] using zeros and reshape
345        let tensor = crate::creation::zeros::<f32>(&[4])
346            .expect("zeros creation failed")
347            .reshape(&[2, 1, 2])
348            .expect("reshape failed");
349
350        // Test squeeze_all (should remove dimension of size 1)
351        let squeezed = tensor.squeeze_all().expect("squeeze_all failed");
352        assert_eq!(squeezed.shape().dims(), &[2, 2]);
353
354        // Test flatten
355        let flattened = tensor.flatten().expect("flatten failed");
356        assert_eq!(flattened.shape().dims(), &[4]);
357
358        // Test flatten_from
359        let flat_from_1 = tensor.flatten_from(1).expect("flatten_from failed");
360        assert_eq!(flat_from_1.shape().dims(), &[2, 2]);
361    }
362
363    #[test]
364    fn test_detach() {
365        let tensor =
366            crate::creation::tensor_1d(&[1.0f32, 2.0, 3.0]).expect("tensor creation failed");
367        let detached = tensor.detach();
368
369        // Should have same data and shape
370        assert_eq!(tensor.shape().dims(), detached.shape().dims());
371        assert_eq!(
372            tensor.data().expect("data retrieval failed"),
373            detached.data().expect("detached data retrieval failed")
374        );
375    }
376
377    #[test]
378    fn test_fluent_api() {
379        use crate::TensorFluentExt;
380        let tensor =
381            crate::creation::tensor_1d(&[1.0f32, 2.0, 3.0, 4.0]).expect("tensor creation failed");
382
383        // Test method chaining with fluent API
384        let result = tensor
385            .fluent()
386            .add_scalar(1.0) // [2.0, 3.0, 4.0, 5.0]
387            .mul_scalar(2.0) // [4.0, 6.0, 8.0, 10.0]
388            .sub_scalar(1.0) // [3.0, 5.0, 7.0, 9.0]
389            .unwrap()
390            .unwrap();
391
392        let expected = vec![3.0, 5.0, 7.0, 9.0];
393        let actual = result.to_vec().expect("to_vec failed");
394
395        for (exp, act) in expected.iter().zip(actual.iter()) {
396            assert!((exp - act).abs() < f32::EPSILON);
397        }
398    }
399
400    #[test]
401    fn test_fluent_api_operations() {
402        use crate::TensorFluentExt;
403        let tensor1 =
404            crate::creation::tensor_1d(&[1.0f32, 2.0, 3.0, 4.0]).expect("tensor1 creation failed");
405        let tensor2 =
406            crate::creation::tensor_1d(&[2.0f32, 2.0, 2.0, 2.0]).expect("tensor2 creation failed");
407
408        // Test tensor operations with fluent API
409        let result = tensor1
410            .fluent()
411            .add(&tensor2) // [3.0, 4.0, 5.0, 6.0]
412            .mul_scalar(0.5) // [1.5, 2.0, 2.5, 3.0]
413            .sum() // 9.0
414            .unwrap()
415            .unwrap();
416
417        let actual = result.to_vec().expect("to_vec failed");
418        assert!((actual[0] - 9.0).abs() < f32::EPSILON);
419    }
420
421    #[test]
422    fn test_fluent_api_mathematical_operations() {
423        use crate::TensorFluentExt;
424        let tensor =
425            crate::creation::tensor_1d(&[1.0f32, 2.0, 3.0, 4.0]).expect("tensor creation failed");
426
427        // Test mathematical operations with fluent API
428        let result = tensor
429            .fluent()
430            .relu() // [1.0, 2.0, 3.0, 4.0] (no change since all positive)
431            .pow(2.0) // [1.0, 4.0, 9.0, 16.0]
432            .sigmoid() // sigmoid values
433            .unwrap()
434            .unwrap();
435
436        let actual = result.to_vec().expect("to_vec failed");
437        // Check that all values are between 0 and 1 (sigmoid property)
438        for val in actual.iter() {
439            assert!(*val > 0.0 && *val < 1.0);
440        }
441    }
442}
443
444/// Fluent API trait for method chaining operations
445///
446/// This trait provides a PyTorch-like fluent interface that allows chaining operations
447/// in a readable and natural way. Unlike lazy evaluation, these operations are executed
448/// immediately but return self to enable chaining.
449///
450/// # Examples
451/// ```rust
452/// use torsh_tensor::{Tensor, TensorFluentExt};
453/// use torsh_core::device::DeviceType;
454///
455/// let result = Tensor::from_data(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
456///     .expect("tensor creation failed")
457///     .fluent()
458///     .add_scalar(1.0)
459///     .mul_scalar(2.0)
460///     .relu()
461///     .sum();
462/// ```
463pub trait TensorFluentExt<T: TensorElement> {
464    /// Start fluent chaining
465    fn fluent(self) -> FluentTensor<T>;
466}
467
468/// Wrapper for fluent tensor operations
469pub struct FluentTensor<T: TensorElement> {
470    tensor: Tensor<T>,
471}
472
473impl<T: TensorElement> TensorFluentExt<T> for Tensor<T> {
474    fn fluent(self) -> FluentTensor<T> {
475        FluentTensor { tensor: self }
476    }
477}
478
479impl<
480        T: TensorElement
481            + Copy
482            + std::ops::Add<Output = T>
483            + std::ops::Sub<Output = T>
484            + std::ops::Mul<Output = T>
485            + std::ops::Div<Output = T>
486            + num_traits::Zero,
487    > FluentTensor<T>
488{
489    /// Get the wrapped tensor, consuming the fluent wrapper
490    pub fn tensor(self) -> Tensor<T> {
491        self.tensor
492    }
493
494    /// Unwrap and return as Result
495    pub fn unwrap(self) -> Result<Tensor<T>> {
496        Ok(self.tensor)
497    }
498
499    /// Chain scalar addition
500    pub fn add_scalar(mut self, scalar: T) -> Self {
501        if let Ok(result) = self.tensor.add_scalar(scalar) {
502            self.tensor = result;
503        }
504        self
505    }
506
507    /// Chain scalar multiplication
508    pub fn mul_scalar(mut self, scalar: T) -> Self {
509        if let Ok(result) = self.tensor.mul_scalar(scalar) {
510            self.tensor = result;
511        }
512        self
513    }
514
515    /// Chain scalar subtraction
516    pub fn sub_scalar(mut self, scalar: T) -> Self {
517        if let Ok(result) = self.tensor.sub_scalar(scalar) {
518            self.tensor = result;
519        }
520        self
521    }
522
523    /// Chain scalar division
524    pub fn div_scalar(mut self, scalar: T) -> Self {
525        if let Ok(result) = self.tensor.div_scalar(scalar) {
526            self.tensor = result;
527        }
528        self
529    }
530
531    /// Chain tensor addition
532    pub fn add(mut self, other: &Tensor<T>) -> Self {
533        if let Ok(result) = self.tensor.add_op(other) {
534            self.tensor = result;
535        }
536        self
537    }
538
539    /// Chain tensor multiplication
540    pub fn mul(mut self, other: &Tensor<T>) -> Self {
541        if let Ok(result) = self.tensor.mul_op(other) {
542            self.tensor = result;
543        }
544        self
545    }
546
547    /// Chain tensor subtraction
548    pub fn sub(mut self, other: &Tensor<T>) -> Self {
549        if let Ok(result) = self.tensor.sub(other) {
550            self.tensor = result;
551        }
552        self
553    }
554
555    /// Chain tensor division
556    pub fn div(mut self, other: &Tensor<T>) -> Self {
557        if let Ok(result) = self.tensor.div(other) {
558            self.tensor = result;
559        }
560        self
561    }
562
563    /// Chain reshape operation
564    pub fn reshape(mut self, shape: &[i32]) -> Self {
565        if let Ok(result) = self.tensor.reshape(shape) {
566            self.tensor = result;
567        }
568        self
569    }
570
571    /// Chain transpose operation
572    pub fn transpose(mut self, dim0: i32, dim1: i32) -> Self {
573        if let Ok(result) = self.tensor.transpose(dim0, dim1) {
574            self.tensor = result;
575        }
576        self
577    }
578
579    /// Chain transpose (last two dimensions)
580    pub fn t(mut self) -> Self {
581        if let Ok(result) = self.tensor.t() {
582            self.tensor = result;
583        }
584        self
585    }
586
587    /// Chain sum operation
588    pub fn sum(mut self) -> Self {
589        if let Ok(result) = self.tensor.sum() {
590            self.tensor = result;
591        }
592        self
593    }
594
595    /// Chain sum along dimension
596    pub fn sum_dim(mut self, dims: &[i32], keepdim: bool) -> Self {
597        if let Ok(result) = self.tensor.sum_dim(dims, keepdim) {
598            self.tensor = result;
599        }
600        self
601    }
602
603    /// Chain squeeze operation
604    pub fn squeeze(mut self, dim: i32) -> Self {
605        if let Ok(result) = self.tensor.squeeze(dim) {
606            self.tensor = result;
607        }
608        self
609    }
610
611    /// Chain unsqueeze operation
612    pub fn unsqueeze(mut self, dim: i32) -> Self {
613        if let Ok(result) = self.tensor.unsqueeze(dim) {
614            self.tensor = result;
615        }
616        self
617    }
618}
619
620/// Mathematical operations for fluent chaining
621impl<T: TensorElement + Copy + num_traits::Float> FluentTensor<T> {
622    /// Chain ReLU activation
623    pub fn relu(mut self) -> Self {
624        if let Ok(result) = self.tensor.relu() {
625            self.tensor = result;
626        }
627        self
628    }
629
630    /// Chain sigmoid activation
631    pub fn sigmoid(mut self) -> Self
632    where
633        T: torsh_core::dtype::FloatElement,
634    {
635        if let Ok(result) = self.tensor.sigmoid() {
636            self.tensor = result;
637        }
638        self
639    }
640
641    /// Chain tanh activation
642    pub fn tanh(mut self) -> Self
643    where
644        T: torsh_core::dtype::FloatElement,
645    {
646        if let Ok(result) = self.tensor.tanh() {
647            self.tensor = result;
648        }
649        self
650    }
651
652    /// Chain exponential function
653    pub fn exp(mut self) -> Self
654    where
655        T: torsh_core::dtype::FloatElement,
656    {
657        if let Ok(result) = self.tensor.exp() {
658            self.tensor = result;
659        }
660        self
661    }
662
663    /// Chain logarithm function
664    pub fn log(mut self) -> Self
665    where
666        T: torsh_core::dtype::FloatElement,
667    {
668        if let Ok(result) = self.tensor.log() {
669            self.tensor = result;
670        }
671        self
672    }
673
674    /// Chain power operation
675    pub fn pow(mut self, exponent: T) -> Self
676    where
677        T: torsh_core::dtype::FloatElement + Into<f32>,
678    {
679        if let Ok(result) = self.tensor.pow(exponent) {
680            self.tensor = result;
681        }
682        self
683    }
684
685    // Note: abs() and neg() methods removed due to complex trait requirements
686    // Users can call these methods directly on the tensor when needed
687}
688
689/// Matrix operations for fluent chaining
690impl<T: TensorElement + Copy> FluentTensor<T>
691where
692    T: num_traits::Float + std::iter::Sum,
693{
694    /// Chain matrix multiplication
695    pub fn matmul(mut self, other: &Tensor<T>) -> Self {
696        if let Ok(result) = self.tensor.matmul(other) {
697            self.tensor = result;
698        }
699        self
700    }
701}
702
703/// Mean operations with specific trait bounds
704impl<
705        T: TensorElement
706            + Copy
707            + num_traits::FromPrimitive
708            + std::ops::Div<Output = T>
709            + num_traits::Zero
710            + num_traits::One,
711    > FluentTensor<T>
712{
713    /// Chain mean operation
714    pub fn mean(mut self, dims: Option<&[usize]>, keepdim: bool) -> Self {
715        if let Ok(result) = self.tensor.mean(dims, keepdim) {
716            self.tensor = result;
717        }
718        self
719    }
720}