Skip to main content

torsh_tensor/
advanced_ops.rs

1//! Advanced tensor operations including reductions, linear algebra, and backend integration
2//!
3//! This module provides high-level tensor operations including reductions, linear algebra
4//! operations, SciRS2 backend integration, and advanced data manipulation functions.
5//!
6//! # Features
7//!
8//! - **Reductions**: max, norm, sum, mean operations
9//! - **Linear algebra**: Matrix multiplication and vector operations
10//! - **SciRS2 integration**: Optimized backend operations for performance
11//! - **Activation functions**: ReLU, sigmoid, tanh through SciRS2 backend
12//! - **Functional programming**: Apply operations and data transformations
13//! - **Memory management**: Copy-on-write semantics and unique data operations
14
15use std::sync::Arc;
16use torsh_core::{
17    device::DeviceType,
18    dtype::{FloatElement, TensorElement},
19    error::{Result, TorshError},
20};
21
22use crate::{
23    core_ops::{Tensor, UnaryKind},
24    storage::TensorStorage,
25};
26
27/// Element count above which a full-tensor sum is folded in parallel chunks.
28const PARALLEL_SUM_THRESHOLD: usize = 65_536;
29
30/// Element count above which the `f32` sum uses the SIMD reduction.
31#[cfg(feature = "simd")]
32const SIMD_SUM_THRESHOLD: usize = 1_024;
33
34/// Sum a contiguous slice, dispatching to SIMD (`f32`) and to threaded chunks
35/// for large inputs, with a scalar fold as the fallback.
36fn sum_slice<T>(data: &[T]) -> T
37where
38    T: TensorElement + Copy + std::ops::Add<Output = T> + num_traits::Zero,
39{
40    let zero = <T as num_traits::Zero>::zero();
41
42    #[cfg(feature = "simd")]
43    {
44        if data.len() >= SIMD_SUM_THRESHOLD
45            && std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>()
46        {
47            use scirs2_core::simd_ops::SimdUnifiedOps;
48            // Safety: TypeId confirmed T == f32, so the slices have identical layout.
49            let as_f32: &[f32] =
50                unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, data.len()) };
51            let total =
52                <f32 as SimdUnifiedOps>::simd_sum(&scirs2_core::ndarray::ArrayView1::from(as_f32));
53            // f32 -> f64 -> T is exact for T == f32; fall through if a type ever
54            // reports f32's TypeId without being convertible.
55            if let Some(value) = <T as TensorElement>::from_f64(f64::from(total)) {
56                return value;
57            }
58        }
59    }
60
61    #[cfg(feature = "parallel")]
62    {
63        if data.len() >= PARALLEL_SUM_THRESHOLD {
64            use scirs2_core::parallel_ops::*;
65            return data
66                .par_chunks(16_384)
67                .map(|chunk| chunk.iter().fold(zero, |acc, &x| acc + x))
68                .reduce(|| zero, |a, b| a + b);
69        }
70    }
71
72    data.iter().fold(zero, |acc, &x| acc + x)
73}
74
75// Float-specific operations
76impl<T: FloatElement + Copy> Tensor<T> {
77    /// Create a 0-dimensional tensor (scalar) from a single value
78    pub fn scalar(value: T) -> Result<Self> {
79        Self::from_data(vec![value], vec![], DeviceType::Cpu)
80    }
81
82    /// Convert tensor to ndarray
83    pub fn as_ndarray(&self) -> Result<scirs2_core::ndarray::ArrayD<T>> {
84        use scirs2_core::ndarray::ArrayD;
85        let data = self.data()?;
86        let shape_obj = self.shape().clone();
87        let shape = shape_obj.dims();
88        ArrayD::from_shape_vec(shape, data.to_vec())
89            .map_err(|e| TorshError::InvalidShape(format!("ndarray conversion failed: {}", e)))
90    }
91
92    /// Create tensor from ndarray
93    pub fn from_ndarray(
94        array: scirs2_core::ndarray::ArrayD<T>,
95        device: DeviceType,
96    ) -> Result<Self> {
97        let shape = array.shape().to_vec();
98        let (data, _offset) = array.into_raw_vec_and_offset();
99        Self::from_data(data, shape, device)
100    }
101}
102
103/// Boolean reduction operations for tensors
104impl<T: TensorElement + Copy> Tensor<T>
105where
106    T: PartialEq + num_traits::Zero,
107{
108    /// Check if all elements are non-zero (true)
109    pub fn all(&self) -> Result<Tensor<bool>> {
110        let data = self.to_vec()?;
111        let zero = <T as num_traits::Zero>::zero();
112        let all_true = data.iter().all(|&x| x != zero);
113        Tensor::from_data(vec![all_true], vec![], self.device())
114    }
115
116    /// Check if any element is non-zero (true)
117    pub fn any(&self) -> Result<Tensor<bool>> {
118        let data = self.to_vec()?;
119        let zero = <T as num_traits::Zero>::zero();
120        let any_true = data.iter().any(|&x| x != zero);
121        Tensor::from_data(vec![any_true], vec![], self.device())
122    }
123
124    /// Check if all elements along dimension are non-zero (true)
125    pub fn all_dim(&self, dim: i32, keepdim: bool) -> Result<Tensor<bool>> {
126        let shape_binding = self.shape();
127        let input_shape = shape_binding.dims();
128
129        let normalized_dim = if dim < 0 {
130            (input_shape.len() as i32 + dim) as usize
131        } else {
132            dim as usize
133        };
134
135        if normalized_dim >= input_shape.len() {
136            return Err(torsh_core::error::TorshError::InvalidDimension {
137                dim: normalized_dim,
138                ndim: input_shape.len(),
139            });
140        }
141
142        let data = self.data()?;
143        let zero = <T as num_traits::Zero>::zero();
144
145        let outer_size: usize = input_shape[..normalized_dim].iter().product();
146        let dim_size = input_shape[normalized_dim];
147        let inner_size: usize = input_shape[normalized_dim + 1..].iter().product();
148
149        let output_size = outer_size * inner_size;
150        let mut result_data = vec![true; output_size];
151
152        for outer in 0..outer_size {
153            for inner in 0..inner_size {
154                let all_nonzero = (0..dim_size).all(|d| {
155                    let idx = outer * dim_size * inner_size + d * inner_size + inner;
156                    data[idx] != zero
157                });
158                let out_idx = outer * inner_size + inner;
159                result_data[out_idx] = all_nonzero;
160            }
161        }
162
163        let mut output_shape = input_shape.to_vec();
164        if keepdim {
165            output_shape[normalized_dim] = 1;
166        } else {
167            output_shape.remove(normalized_dim);
168        }
169
170        Tensor::<bool>::from_data(result_data, output_shape, self.device())
171    }
172
173    /// Check if any element along dimension is non-zero (true)
174    pub fn any_dim(&self, dim: i32, keepdim: bool) -> Result<Tensor<bool>> {
175        let shape_binding = self.shape();
176        let input_shape = shape_binding.dims();
177
178        let normalized_dim = if dim < 0 {
179            (input_shape.len() as i32 + dim) as usize
180        } else {
181            dim as usize
182        };
183
184        if normalized_dim >= input_shape.len() {
185            return Err(torsh_core::error::TorshError::InvalidDimension {
186                dim: normalized_dim,
187                ndim: input_shape.len(),
188            });
189        }
190
191        let data = self.data()?;
192        let zero = <T as num_traits::Zero>::zero();
193
194        let outer_size: usize = input_shape[..normalized_dim].iter().product();
195        let dim_size = input_shape[normalized_dim];
196        let inner_size: usize = input_shape[normalized_dim + 1..].iter().product();
197
198        let output_size = outer_size * inner_size;
199        let mut result_data = vec![false; output_size];
200
201        for outer in 0..outer_size {
202            for inner in 0..inner_size {
203                let any_nonzero = (0..dim_size).any(|d| {
204                    let idx = outer * dim_size * inner_size + d * inner_size + inner;
205                    data[idx] != zero
206                });
207                let out_idx = outer * inner_size + inner;
208                result_data[out_idx] = any_nonzero;
209            }
210        }
211
212        let mut output_shape = input_shape.to_vec();
213        if keepdim {
214            output_shape[normalized_dim] = 1;
215        } else {
216            output_shape.remove(normalized_dim);
217        }
218
219        Tensor::<bool>::from_data(result_data, output_shape, self.device())
220    }
221}
222
223// General tensor operations
224impl<T: TensorElement + Copy> Tensor<T> {
225    /// Compute sum of all elements
226    ///
227    /// Reads the storage in place (no intermediate copy) and, for large
228    /// tensors, folds chunks in parallel through `scirs2_core::parallel_ops`.
229    pub fn sum(&self) -> Result<Self>
230    where
231        T: std::ops::Add<Output = T> + num_traits::Zero,
232    {
233        let sum_value = self.with_contiguous_data(|data| Ok(sum_slice(data)))?;
234        let mut result = Tensor::from_data(vec![sum_value], vec![], self.device())?;
235
236        // Record the sum operation for autograd: d(sum)/dx_i = 1 for every element.
237        if crate::should_record_grad(self.requires_grad) {
238            result.requires_grad = true;
239            result.operation = crate::core_ops::Operation::Sum {
240                input: Arc::new(self.clone()),
241            };
242        }
243
244        Ok(result)
245    }
246
247    /// Global minimum of the tensor (see [`Tensor::amin`] for the dimension-aware
248    /// form, and [`Tensor::min_dim`] for a single axis).
249    pub fn min(&self) -> Result<Self>
250    where
251        T: std::cmp::PartialOrd + Copy,
252    {
253        let min_val = self.with_contiguous_data(|data| {
254            let mut iter = data.iter().copied();
255            let first = iter.next().ok_or_else(|| {
256                TorshError::InvalidOperation("Cannot compute min of empty tensor".to_string())
257            })?;
258            Ok(iter.fold(first, |acc, x| if x < acc { x } else { acc }))
259        })?;
260        Self::from_data(vec![min_val], vec![], self.device)
261    }
262
263    /// Transpose operation (2D tensor)
264    ///
265    /// Produces exactly what [`Tensor::transpose(0, 1)`](Tensor::transpose)
266    /// produces, including its autograd node: a transpose is the permutation
267    /// that swaps the two axes, recorded as
268    /// [`ViewKind::Permute`](crate::core_ops::ViewKind::Permute) so the backward
269    /// pass permutes the gradient straight back. Building the result with a
270    /// private `from_data` copy instead — which is what this used to do — made
271    /// `t()` an honestly-detached leaf: `x.t()` came back with
272    /// `requires_grad == false` even from a `requires_grad` input, so any loss
273    /// routed through a transpose silently lost its gradient path.
274    ///
275    /// The rank check is kept: unlike `transpose`, `t()` is defined only for
276    /// matrices and still rejects any other rank.
277    ///
278    /// # Layout
279    /// The result is a *strided view* of `self`, exactly like `transpose`'s (no
280    /// data is copied). Callers that need a packed buffer call `.contiguous()`;
281    /// callers that read it through [`Tensor::to_vec`] or
282    /// `with_contiguous_data` see the transposed order either way.
283    pub fn t(&self) -> Result<Self>
284    where
285        T: Copy + num_traits::Zero,
286    {
287        let shape = self.shape();
288        let dims = shape.dims();
289
290        if dims.len() != 2 {
291            return Err(TorshError::InvalidOperation(
292                "Transpose operation only supported for 2D tensors".to_string(),
293            ));
294        }
295
296        self.transpose(0, 1)
297    }
298
299    /// Check if two tensors share the same underlying storage
300    pub fn shares_storage(&self, other: &Self) -> bool {
301        // For storage abstraction, we need to check the underlying storage
302        match (&self.storage, &other.storage) {
303            (TensorStorage::InMemory(a), TensorStorage::InMemory(b)) => Arc::ptr_eq(a, b),
304            (TensorStorage::MemoryMapped(a), TensorStorage::MemoryMapped(b)) => Arc::ptr_eq(a, b),
305            // Without this arm the catch-all below would report two aliases of
306            // one device allocation as unshared, and `make_unique` would skip a
307            // copy it needs.
308            #[cfg(feature = "gpu")]
309            (TensorStorage::Device { buffer: a, .. }, TensorStorage::Device { buffer: b, .. }) => {
310                Arc::ptr_eq(a, b)
311            }
312            _ => false,
313        }
314    }
315
316    /// Get data as a vector (backward compatibility method)
317    pub fn data(&self) -> Result<Vec<T>>
318    where
319        T: Copy,
320    {
321        self.to_vec()
322    }
323
324    /// Run `f` against a contiguous, view-ordered slice of this tensor's data.
325    ///
326    /// Base tensors hand the storage slice straight to the closure (zero copy).
327    /// Strided views are materialised in view order first, so callers can always
328    /// index the slice with plain row-major arithmetic.
329    pub(crate) fn with_contiguous_data<R, F>(&self, f: F) -> Result<R>
330    where
331        F: FnOnce(&[T]) -> Result<R>,
332        T: Copy,
333    {
334        let numel = self.numel();
335        if self.is_view() || self.strides.is_some() || self.storage_offset != 0 {
336            let data = self.to_vec()?;
337            return f(&data);
338        }
339
340        self.storage.with_slice(|slice| {
341            if slice.len() < numel {
342                return Err(TorshError::InvalidOperation(format!(
343                    "storage holds {} elements but the tensor shape needs {}",
344                    slice.len(),
345                    numel
346                )));
347            }
348            f(&slice[..numel])
349        })
350    }
351
352    /// Materialise this tensor as a contiguous, uniquely owned, *mutable* base
353    /// tensor.
354    ///
355    /// This is the single entry point used by every in-place operation. It
356    /// compacts strided views into view order **and** resets the view metadata
357    /// (`strides`, `storage_offset`, `base_tensor`), which is what keeps later
358    /// reads from applying the permutation a second time.
359    ///
360    /// Note that a view detaches from its base here: subsequent writes are no
361    /// longer visible through the original tensor.
362    pub(crate) fn materialize_contiguous(&mut self) -> Result<()>
363    where
364        T: Copy,
365    {
366        let data_vec = self.to_vec()?;
367        self.storage = Self::mutable_storage(data_vec)?;
368        self.strides = None;
369        self.storage_offset = 0;
370        self.base_tensor = None;
371        Ok(())
372    }
373
374    /// Build storage that supports `with_slice_mut` (i.e. never `SimdOptimized`).
375    fn mutable_storage(data: Vec<T>) -> Result<TensorStorage<T>> {
376        #[cfg(feature = "simd")]
377        {
378            // Aligned storage keeps SIMD-friendly alignment while staying mutable.
379            const MEMORY_MAPPING_BYTES: usize = 1024 * 1024 * 1024;
380            if data.len() * std::mem::size_of::<T>() < MEMORY_MAPPING_BYTES {
381                return TensorStorage::aligned(data);
382            }
383        }
384        TensorStorage::create_optimal(data)
385    }
386
387    /// Apply a function to all elements in-place using direct storage access
388    pub fn data_mut_apply<F>(&mut self, mut func: F) -> Result<()>
389    where
390        F: FnMut(&mut T),
391        T: Copy,
392    {
393        self.prepare_for_inplace()?;
394
395        match &self.storage {
396            TensorStorage::MemoryMapped(_) => {
397                // Memory-mapped storage has no mutable slice access: read, modify, write back.
398                let mut new_data = self.to_vec()?;
399                for item in new_data.iter_mut() {
400                    func(item);
401                }
402                self.storage = TensorStorage::create_optimal(new_data)?;
403                Ok(())
404            }
405            _ => {
406                let numel = self.numel();
407                self.storage.with_slice_mut(|slice| {
408                    for item in slice.iter_mut().take(numel) {
409                        func(item);
410                    }
411                    Ok(())
412                })
413            }
414        }
415    }
416
417    /// Clone the tensor with independent data (deep copy)
418    pub fn clone_data(&self) -> Self
419    where
420        T: Copy,
421    {
422        let data = self
423            .to_vec()
424            .expect("tensor to vec conversion should succeed");
425        Self::from_data(data, self.shape().dims().to_vec(), self.device)
426            .expect("tensor creation should succeed")
427    }
428
429    /// Ensure tensor has unique data (copy-on-write semantics)
430    ///
431    /// Strided views are compacted *and* detached (see
432    /// [`Tensor::materialize_contiguous`]); base tensors are copied only when
433    /// their storage is actually shared — including `SimdOptimized` storage,
434    /// which implements copy-on-write internally and therefore needs no eager
435    /// promotion when this tensor is its only owner.
436    pub fn make_unique(&mut self) -> Result<()> {
437        if self.is_view() || self.strides.is_some() || self.storage_offset != 0 {
438            return self.materialize_contiguous();
439        }
440
441        match &self.storage {
442            TensorStorage::InMemory(data) => {
443                if Arc::strong_count(data) > 1 {
444                    let data_vec = self.to_vec()?;
445                    self.storage = Self::mutable_storage(data_vec)?;
446                }
447            }
448            TensorStorage::MemoryMapped(storage) => {
449                if Arc::strong_count(storage) > 1 {
450                    let data_vec = self.to_vec()?;
451                    self.storage = TensorStorage::create_optimal(data_vec)?;
452                }
453            }
454            #[cfg(feature = "simd")]
455            TensorStorage::Aligned(data) => {
456                if Arc::strong_count(data) > 1 {
457                    let data_vec = self.to_vec()?;
458                    self.storage = Self::mutable_storage(data_vec)?;
459                }
460            }
461            #[cfg(feature = "simd")]
462            TensorStorage::SimdOptimized(storage) => {
463                // `SimdStorage` is read-optimised: it never releases its
464                // `original` buffer, so writing through its copy-on-write path
465                // would leave every mutated tensor holding two full buffers for
466                // the rest of its life. Promote to `Aligned` instead, which is a
467                // single resident buffer that `with_slice_mut` can mutate in
468                // place from then on.
469                //
470                // The promotion copies the data straight out of the SIMD
471                // storage: the previous `to_vec()` + `aligned()` pair allocated
472                // and copied the whole tensor twice.
473                let promoted = storage.with_slice(TensorStorage::aligned_from_slice)?;
474                self.storage = promoted;
475            }
476            #[cfg(feature = "gpu")]
477            TensorStorage::Device { .. } => {
478                // A device buffer is immutable and has no offset-capable write
479                // primitive, so making a tensor writable *always* means moving
480                // it back to the host. The download is the storage's cached one,
481                // so this costs at most a single transfer.
482                let data_vec = self.to_vec()?;
483                self.storage = Self::mutable_storage(data_vec)?;
484            }
485        }
486        Ok(())
487    }
488
489    /// Prepare this tensor for an in-place write: contiguous, uniquely owned and
490    /// backed by mutable storage.
491    pub(crate) fn prepare_for_inplace(&mut self) -> Result<()>
492    where
493        T: Copy,
494    {
495        self.make_unique()
496    }
497
498    /// Apply function in-place
499    ///
500    /// Mutates the storage buffer directly - no temporary buffer is allocated
501    /// for base tensors that are already uniquely owned.
502    pub fn apply_<F>(&mut self, func: F) -> Result<()>
503    where
504        F: Fn(T) -> T,
505        T: Copy,
506    {
507        self.prepare_for_inplace()?;
508
509        if matches!(self.storage, TensorStorage::MemoryMapped(_)) {
510            // Memory-mapped storage cannot expose a mutable slice.
511            let data = self.to_vec()?;
512            let new_data: Vec<T> = data.into_iter().map(func).collect();
513            self.storage = TensorStorage::create_optimal(new_data)?;
514            return Ok(());
515        }
516
517        let numel = self.numel();
518        self.storage.with_slice_mut(|slice| {
519            for value in slice.iter_mut().take(numel) {
520                *value = func(*value);
521            }
522            Ok(())
523        })
524    }
525
526    /// Apply function element-wise to create new tensor
527    ///
528    /// Allocates exactly one output buffer and fills it from a borrowed view of
529    /// the input.
530    ///
531    /// **Forward-only: the result is a detached leaf.** A closure carries no
532    /// derivative, so propagating `requires_grad` here would manufacture a
533    /// `requires_grad` node whose operation is still
534    /// [`Operation::Leaf`](crate::core_ops::Operation::Leaf) — and the backward
535    /// pass stops at every leaf and accumulates into that leaf's own
536    /// (unreachable) gradient slot, which makes `backward()` succeed with
537    /// silently wrong numbers. Callers that *are* differentiable must therefore
538    /// record their own [`Operation`](crate::core_ops::Operation) on the result
539    /// (see `Tensor::record_unary` and the `MulScalar`/`AddScalar` idiom in
540    /// `math_ops.rs`).
541    pub fn map<F>(&self, func: F) -> Result<Self>
542    where
543        F: Fn(T) -> T,
544        T: Copy,
545    {
546        let new_data = self.with_contiguous_data(|data| {
547            let mut out = Vec::with_capacity(data.len());
548            out.extend(data.iter().map(|&x| func(x)));
549            Ok(out)
550        })?;
551        Self::from_data(new_data, self.shape().dims().to_vec(), self.device)
552    }
553
554    /// Extract a scalar value from a single-element tensor
555    pub fn item(&self) -> Result<T>
556    where
557        T: Copy,
558    {
559        let data = self.data()?;
560        if data.len() != 1 {
561            return Err(TorshError::InvalidArgument(format!(
562                "item() can only be called on single-element tensors, got {} elements",
563                data.len()
564            )));
565        }
566        Ok(data[0])
567    }
568
569    /// Concatenate tensors along a dimension
570    pub fn cat(tensors: &[&Self], dim: i32) -> Result<Self>
571    where
572        T: Copy,
573    {
574        if tensors.is_empty() {
575            return Err(TorshError::InvalidArgument(
576                "Cannot concatenate empty tensor list".to_string(),
577            ));
578        }
579
580        let first_shape_binding = tensors[0].shape();
581        let first_shape = first_shape_binding.dims();
582        let ndim = first_shape.len();
583
584        // Normalize dim (allow negative indexing)
585        let actual_dim = if dim < 0 {
586            (ndim as i32 + dim) as usize
587        } else {
588            dim as usize
589        };
590
591        if actual_dim >= ndim {
592            return Err(TorshError::InvalidArgument(format!(
593                "Dimension {} out of range for {}-dimensional tensor",
594                dim, ndim
595            )));
596        }
597
598        // Validate all tensors have compatible shapes (same on all dims except actual_dim)
599        for (i, tensor) in tensors.iter().enumerate().skip(1) {
600            let shape_binding = tensor.shape();
601            let shape = shape_binding.dims();
602            if shape.len() != ndim {
603                return Err(TorshError::InvalidArgument(format!(
604                    "Tensor {} has {} dimensions but first tensor has {}",
605                    i,
606                    shape.len(),
607                    ndim
608                )));
609            }
610            for (d, (&s1, &s2)) in first_shape.iter().zip(shape.iter()).enumerate() {
611                if d != actual_dim && s1 != s2 {
612                    return Err(TorshError::ShapeMismatch {
613                        expected: first_shape.to_vec(),
614                        got: shape.to_vec(),
615                    });
616                }
617            }
618        }
619
620        // Compute output shape: same as input except actual_dim is sum of all cat dims
621        let cat_dim_total: usize = tensors.iter().map(|t| t.shape().dims()[actual_dim]).sum();
622        let mut result_shape = first_shape.to_vec();
623        result_shape[actual_dim] = cat_dim_total;
624
625        // Gather all data in order, interleaving elements for proper layout
626        // Outer = product of dims before actual_dim
627        // Cat stride = product of dims after actual_dim (inner)
628        let outer_size: usize = first_shape[..actual_dim].iter().product();
629        let inner_size: usize = first_shape[actual_dim + 1..].iter().product();
630
631        let total_numel: usize = result_shape.iter().product();
632        let mut result_data = Vec::with_capacity(total_numel);
633
634        // Materialise every input exactly once (previously this happened once per
635        // (outer, tensor) pair, i.e. `outer_size` full copies of every input).
636        let sources: Vec<Vec<T>> = tensors
637            .iter()
638            .map(|tensor| tensor.to_vec())
639            .collect::<Result<Vec<_>>>()?;
640        let cat_sizes: Vec<usize> = tensors
641            .iter()
642            .map(|tensor| tensor.shape().dims()[actual_dim])
643            .collect();
644
645        for outer in 0..outer_size {
646            for (source, &cat_size) in sources.iter().zip(cat_sizes.iter()) {
647                // One contiguous run per (outer, tensor) pair.
648                let run = cat_size * inner_size;
649                let start = outer * run;
650                result_data.extend_from_slice(&source[start..start + run]);
651            }
652        }
653
654        let mut result = Self::from_data(result_data, result_shape, tensors[0].device)?;
655
656        // Record the concatenation so gradients split back to each input along
657        // `actual_dim`. Only recorded when at least one input tracks gradients.
658        let any_requires_grad = tensors.iter().any(|t| t.requires_grad);
659        if crate::should_record_grad(any_requires_grad) {
660            result.requires_grad = true;
661            result.operation = crate::core_ops::Operation::Concat {
662                inputs: tensors.iter().map(|t| Arc::new((*t).clone())).collect(),
663                dim: actual_dim,
664            };
665        }
666
667        Ok(result)
668    }
669}
670
671// Numeric operations
672impl<T: TensorElement + Copy> Tensor<T>
673where
674    T: num_traits::Float,
675{
676    /// Compute the L2 norm of the tensor
677    pub fn norm(&self) -> Result<Self> {
678        let data = self.data()?;
679        let sum_squares: T = data
680            .iter()
681            .map(|&x| x * x)
682            .fold(num_traits::Zero::zero(), |acc, x| acc + x);
683        let norm_value = sum_squares.sqrt();
684
685        // Return scalar tensor (1-element tensor with shape [])
686        Tensor::from_data(vec![norm_value], vec![], self.device())
687    }
688
689    /// Computes the p-norm (Lp norm) of the tensor, optionally reduced along
690    /// specific dimensions.
691    ///
692    /// Mirrors PyTorch's `torch.norm(p, dim, keepdim)` semantics:
693    /// - `p == 1.0` -> L1 ("Manhattan") norm: `sum(|x|)`
694    /// - `p == 2.0` -> L2 (Euclidean) norm: `sqrt(sum(x^2))`, matching [`Tensor::norm`]
695    /// - `p == 0.0` -> count of non-zero elements
696    /// - `p == f64::INFINITY` -> maximum absolute value
697    /// - `p == f64::NEG_INFINITY` -> minimum absolute value
698    /// - any other finite `p` -> general Lp norm: `(sum(|x|^p))^(1/p)`
699    ///
700    /// `dims == None` reduces over every element, producing a scalar tensor
701    /// (or an all-ones-shaped tensor when `keepdim` is true). `dims ==
702    /// Some(&[...])` reduces only the given dimensions, which must already be
703    /// normalized (non-negative and in range); duplicates are ignored.
704    pub fn norm_lp(&self, p: f64, dims: Option<&[usize]>, keepdim: bool) -> Result<Self>
705    where
706        T: num_traits::FromPrimitive,
707    {
708        let shape_binding = self.shape();
709        let input_shape = shape_binding.dims().to_vec();
710        let ndim = input_shape.len();
711
712        let reduce_dims: Vec<usize> = match dims {
713            Some(requested) => {
714                for &dim in requested {
715                    if dim >= ndim {
716                        return Err(TorshError::InvalidOperation(format!(
717                            "Dimension {} out of range for {}-dimensional tensor",
718                            dim, ndim
719                        )));
720                    }
721                }
722                let mut normalized = requested.to_vec();
723                normalized.sort_unstable();
724                normalized.dedup();
725                normalized
726            }
727            None => (0..ndim).collect(),
728        };
729
730        let convert = |value: f64| -> Result<T> {
731            <T as num_traits::FromPrimitive>::from_f64(value).ok_or_else(|| {
732                TorshError::InvalidOperation(format!(
733                    "norm: p={} cannot be represented in this tensor's element type",
734                    p
735                ))
736            })
737        };
738
739        let zero = <T as num_traits::Zero>::zero();
740        let one = <T as num_traits::One>::one();
741
742        #[derive(Clone, Copy, PartialEq)]
743        enum NormKind {
744            L0,
745            L1,
746            L2,
747            MaxAbs,
748            MinAbs,
749            General,
750        }
751
752        let kind = if p == 1.0 {
753            NormKind::L1
754        } else if p == 2.0 {
755            NormKind::L2
756        } else if p == 0.0 {
757            NormKind::L0
758        } else if p == f64::INFINITY {
759            NormKind::MaxAbs
760        } else if p == f64::NEG_INFINITY {
761            NormKind::MinAbs
762        } else {
763            NormKind::General
764        };
765
766        let (p_t, inv_p_t) = if kind == NormKind::General {
767            (convert(p)?, convert(1.0 / p)?)
768        } else {
769            (zero, zero)
770        };
771
772        // Identity element for the combining operation (sum -> 0, min -> +inf).
773        let init: T = if kind == NormKind::MinAbs {
774            <T as num_traits::Float>::infinity()
775        } else {
776            zero
777        };
778
779        let elem = |x: T| -> T {
780            match kind {
781                NormKind::L1 | NormKind::MaxAbs | NormKind::MinAbs => x.abs(),
782                NormKind::L2 => x * x,
783                NormKind::L0 => {
784                    if x == zero {
785                        zero
786                    } else {
787                        one
788                    }
789                }
790                NormKind::General => x.abs().powf(p_t),
791            }
792        };
793
794        let combine = |a: T, b: T| -> T {
795            match kind {
796                NormKind::MaxAbs => a.max(b),
797                NormKind::MinAbs => a.min(b),
798                _ => a + b,
799            }
800        };
801
802        let finalize = |s: T| -> T {
803            match kind {
804                NormKind::L2 => s.sqrt(),
805                NormKind::General => s.powf(inv_p_t),
806                _ => s,
807            }
808        };
809
810        let data = self.data()?;
811
812        // Fully reduced (global norm) fast path: every dimension collapses to a scalar.
813        if ndim == 0 || reduce_dims.len() == ndim {
814            let acc = data.iter().fold(init, |acc, &x| combine(acc, elem(x)));
815            let value = finalize(acc);
816            let out_shape = if keepdim { vec![1; ndim] } else { vec![] };
817            return Self::from_data(vec![value], out_shape, self.device());
818        }
819
820        // Partial reduction over an arbitrary subset of dimensions: walk every
821        // element once, mapping its flat input index to the flat index of the
822        // (keepdim-shaped) output it accumulates into.
823        let mut is_reduced = vec![false; ndim];
824        for &d in &reduce_dims {
825            is_reduced[d] = true;
826        }
827
828        let mut input_strides = vec![1usize; ndim];
829        for i in (0..ndim - 1).rev() {
830            input_strides[i] = input_strides[i + 1] * input_shape[i + 1];
831        }
832
833        let mut output_shape_keepdim = input_shape.clone();
834        for &d in &reduce_dims {
835            output_shape_keepdim[d] = 1;
836        }
837        let mut output_strides = vec![1usize; ndim];
838        for i in (0..ndim - 1).rev() {
839            output_strides[i] = output_strides[i + 1] * output_shape_keepdim[i + 1];
840        }
841        let output_size: usize = output_shape_keepdim.iter().product();
842
843        let mut acc = vec![init; output_size];
844        for (flat_idx, &x) in data.iter().enumerate() {
845            let mut remaining = flat_idx;
846            let mut out_flat = 0usize;
847            for (dim, &reduced) in is_reduced.iter().enumerate() {
848                let coord = remaining / input_strides[dim];
849                remaining %= input_strides[dim];
850                if !reduced {
851                    out_flat += coord * output_strides[dim];
852                }
853            }
854            acc[out_flat] = combine(acc[out_flat], elem(x));
855        }
856
857        let result_data: Vec<T> = acc.into_iter().map(finalize).collect();
858
859        let final_shape = if keepdim {
860            output_shape_keepdim
861        } else {
862            input_shape
863                .into_iter()
864                .zip(is_reduced.iter())
865                .filter(|(_, &reduced)| !reduced)
866                .map(|(size, _)| size)
867                .collect::<Vec<_>>()
868        };
869
870        Self::from_data(result_data, final_shape, self.device())
871    }
872}
873
874// SciRS2 backend integration (placeholder implementations)
875impl<T: TensorElement + Copy> Tensor<T> {
876    /// Use SciRS2 backend for optimized matrix multiplication
877    pub fn matmul_scirs2(&self, other: &Self) -> Result<Self>
878    where
879        T: num_traits::Float + num_traits::Zero + num_traits::One + std::iter::Sum,
880    {
881        // TODO: Integrate with actual SciRS2 backend
882        // For now, implement basic matrix multiplication
883        self.basic_matmul(other)
884    }
885
886    /// Use SciRS2 backend for optimized sum reduction
887    pub fn sum_scirs2(&self) -> Result<Self>
888    where
889        T: std::ops::Add<Output = T> + num_traits::Zero,
890    {
891        // TODO: Integrate with actual SciRS2 backend
892        let data = self.data()?;
893        let sum_value = data
894            .iter()
895            .fold(<T as num_traits::Zero>::zero(), |acc, &x| acc + x);
896        Tensor::from_data(vec![sum_value], vec![], self.device())
897    }
898
899    /// Use SciRS2 backend for optimized mean reduction
900    pub fn mean_scirs2(&self) -> Result<Self>
901    where
902        T: std::ops::Add<Output = T>
903            + std::ops::Div<Output = T>
904            + num_traits::Zero
905            + From<usize>
906            + num_traits::FromPrimitive,
907    {
908        // TODO: Integrate with actual SciRS2 backend
909        let data = self.data()?;
910        if data.is_empty() {
911            return Err(TorshError::InvalidArgument(
912                "Cannot compute mean of empty tensor".to_string(),
913            ));
914        }
915        let sum_value = data
916            .iter()
917            .fold(<T as num_traits::Zero>::zero(), |acc, &x| acc + x);
918        let mean_value = sum_value / T::from(data.len());
919        Tensor::from_data(vec![mean_value], vec![], self.device())
920    }
921
922    /// Use SciRS2 backend for optimized ReLU activation
923    ///
924    /// Records the same [`UnaryKind::Relu`] as [`Tensor::relu`]: the forward
925    /// predicate (`x > 0`) is identical, just computed through a bare `map`
926    /// instead of `relu`'s SIMD/parallel dispatch, so the two must share one
927    /// recorded derivative rather than one silently detaching.
928    pub fn relu_scirs2(&self) -> Result<Self>
929    where
930        T: PartialOrd + num_traits::Zero,
931    {
932        // TODO: Integrate with actual SciRS2 backend
933        let zero = <T as num_traits::Zero>::zero();
934        let result = self.map(|x| if x > zero { x } else { zero })?;
935        Ok(self.record_unary(result, UnaryKind::Relu))
936    }
937
938    /// Use SciRS2 backend for optimized sigmoid activation
939    ///
940    /// Records the same [`UnaryKind::Sigmoid`] as [`Tensor::sigmoid`]: this is
941    /// the exact closed form (not an approximation), so it is numerically
942    /// consistent with `Sigmoid`'s recorded derivative on every input.
943    pub fn sigmoid_scirs2(&self) -> Result<Self>
944    where
945        T: num_traits::Float,
946    {
947        // TODO: Integrate with actual SciRS2 backend
948        let result = self.map(|x| {
949            let one = <T as num_traits::One>::one();
950            one / (one + (-x).exp())
951        })?;
952        Ok(self.record_unary(result, UnaryKind::Sigmoid))
953    }
954
955    /// Use SciRS2 backend for optimized tanh activation
956    ///
957    /// Records the same [`UnaryKind::Tanh`] as [`Tensor::tanh`].
958    pub fn tanh_scirs2(&self) -> Result<Self>
959    where
960        T: num_traits::Float,
961    {
962        // TODO: Integrate with actual SciRS2 backend
963        let result = self.map(|x| x.tanh())?;
964        Ok(self.record_unary(result, UnaryKind::Tanh))
965    }
966
967    /// Softmax activation along specified dimension
968    /// Computes softmax(x_i) = exp(x_i) / sum(exp(x_j)) for all j
969    pub fn softmax(&self, dim: i32) -> Result<Self>
970    where
971        T: torsh_core::dtype::FloatElement
972            + Copy
973            + std::ops::Sub<Output = T>
974            + std::ops::Div<Output = T>,
975    {
976        let data = self.data()?;
977        let shape_binding = self.shape();
978        let shape = shape_binding.dims();
979
980        // Validate tensor has data
981        if data.is_empty() || shape.is_empty() {
982            return Err(TorshError::InvalidOperation(
983                "Cannot compute softmax on empty tensor".to_string(),
984            ));
985        }
986
987        // Handle negative dimension
988        let actual_dim = if dim < 0 {
989            (shape.len() as i32 + dim) as usize
990        } else {
991            dim as usize
992        };
993
994        if actual_dim >= shape.len() {
995            return Err(TorshError::InvalidOperation(format!(
996                "Dimension {} out of range for {}-dimensional tensor",
997                actual_dim,
998                shape.len()
999            )));
1000        }
1001
1002        // For numerical stability, subtract max before exp
1003        let max_tensor = self.max(Some(actual_dim), true)?;
1004
1005        // Expand max_tensor to match input shape for broadcasting
1006        let expanded_max = max_tensor.expand(shape)?;
1007        let shifted = self.sub(&expanded_max)?;
1008        let exp_tensor = shifted.exp()?;
1009        let sum_tensor = exp_tensor.sum_dim(&[actual_dim as i32], true)?;
1010
1011        // Expand sum_tensor to match exp_tensor shape for broadcasting
1012        let expanded_sum = sum_tensor.expand(shape)?;
1013        exp_tensor.div(&expanded_sum)
1014    }
1015
1016    /// Log softmax activation along specified dimension.
1017    ///
1018    /// Computed with the log-sum-exp identity
1019    /// `log_softmax(x) = (x - max) - log(sum(exp(x - max)))` rather than as
1020    /// `log(softmax(x))`. The naive form underflows: once a logit sits about 90
1021    /// (`f32`) below the row maximum, `exp` rounds it to exactly `0` and the
1022    /// following `log` returns `-inf`, which then poisons every downstream loss
1023    /// (this is why cross-entropy over sharply scaled logits used to be `inf`).
1024    /// Here the sum is at least `1` — the maximum contributes `exp(0)` — so the
1025    /// logarithm never sees zero and every output stays finite.
1026    pub fn log_softmax(&self, dim: i32) -> Result<Self>
1027    where
1028        T: torsh_core::dtype::FloatElement + Copy + std::ops::Sub<Output = T>,
1029    {
1030        let shape_binding = self.shape();
1031        let shape = shape_binding.dims().to_vec();
1032
1033        if shape.is_empty() {
1034            return Err(TorshError::InvalidOperation(
1035                "Cannot compute log_softmax on empty tensor".to_string(),
1036            ));
1037        }
1038
1039        let actual_dim = if dim < 0 {
1040            (shape.len() as i32 + dim) as usize
1041        } else {
1042            dim as usize
1043        };
1044
1045        if actual_dim >= shape.len() {
1046            return Err(TorshError::InvalidOperation(format!(
1047                "Dimension {} out of range for {}-dimensional tensor",
1048                actual_dim,
1049                shape.len()
1050            )));
1051        }
1052
1053        // Shift by the per-slice maximum so the largest exponent is exp(0) = 1.
1054        let max_tensor = self.max(Some(actual_dim), true)?;
1055        let expanded_max = max_tensor.expand(&shape)?;
1056        let shifted = self.sub(&expanded_max)?;
1057
1058        // log(sum(exp(shifted))) is finite: the sum is bounded below by 1.
1059        let sum_exp = shifted.exp()?.sum_dim(&[actual_dim as i32], true)?;
1060        let log_sum_exp = sum_exp.log()?;
1061        let expanded_log_sum = log_sum_exp.expand(&shape)?;
1062
1063        let mut result = shifted.sub(&expanded_log_sum)?;
1064
1065        // Record the exact, stable log-softmax Jacobian instead of the composed
1066        // (partly detaching) sub/exp/sum/log graph. Overwriting the operation
1067        // discards those throwaway intermediates. This is what makes
1068        // cross-entropy differentiable end-to-end.
1069        if crate::should_record_grad(self.requires_grad) {
1070            result.requires_grad = true;
1071            result.operation = crate::core_ops::Operation::LogSoftmax {
1072                input: Arc::new(self.clone()),
1073                dim: actual_dim,
1074            };
1075        }
1076
1077        Ok(result)
1078    }
1079
1080    /// Returns the k largest elements along a dimension
1081    pub fn topk(
1082        &self,
1083        k: usize,
1084        dim: Option<i32>,
1085        largest: bool,
1086        sorted: bool,
1087    ) -> Result<(Self, Tensor<i64>)>
1088    where
1089        T: std::cmp::PartialOrd + Copy + num_traits::Zero,
1090    {
1091        let data = self.data()?;
1092        let shape_binding = self.shape();
1093        let shape = shape_binding.dims();
1094
1095        if shape.is_empty() {
1096            return Err(TorshError::InvalidOperation(
1097                "Cannot compute topk on empty tensor".to_string(),
1098            ));
1099        }
1100
1101        if k == 0 {
1102            return Err(TorshError::InvalidArgument(
1103                "k must be greater than 0".to_string(),
1104            ));
1105        }
1106
1107        // Determine actual dimension to operate on (default: last dim)
1108        let actual_dim = match dim {
1109            Some(d) => {
1110                let norm = if d < 0 {
1111                    (shape.len() as i32 + d) as usize
1112                } else {
1113                    d as usize
1114                };
1115                if norm >= shape.len() {
1116                    return Err(TorshError::InvalidArgument(format!(
1117                        "Dimension {} out of range for {}-dimensional tensor",
1118                        d,
1119                        shape.len()
1120                    )));
1121                }
1122                norm
1123            }
1124            None => shape.len() - 1,
1125        };
1126
1127        let dim_size = shape[actual_dim];
1128        let effective_k = k.min(dim_size);
1129
1130        let outer_size: usize = shape[..actual_dim].iter().product();
1131        let inner_size: usize = shape[actual_dim + 1..].iter().product();
1132
1133        // Output shape: same as input but actual_dim replaced with k
1134        let mut result_shape = shape.to_vec();
1135        result_shape[actual_dim] = effective_k;
1136
1137        let mut values_data = Vec::with_capacity(outer_size * effective_k * inner_size);
1138        let mut indices_data = Vec::with_capacity(outer_size * effective_k * inner_size);
1139
1140        for outer in 0..outer_size {
1141            for inner in 0..inner_size {
1142                // Gather (local_index, value) pairs along actual_dim for this (outer, inner) slice
1143                let mut slice: Vec<(usize, T)> = (0..dim_size)
1144                    .map(|d| {
1145                        let src = outer * dim_size * inner_size + d * inner_size + inner;
1146                        (d, data[src])
1147                    })
1148                    .collect();
1149
1150                // Sort by value to find top-k candidates
1151                if largest {
1152                    slice
1153                        .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1154                } else {
1155                    slice
1156                        .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1157                }
1158
1159                let mut top_k: Vec<(usize, T)> = slice.into_iter().take(effective_k).collect();
1160
1161                // When sorted=false, restore original (position) order
1162                if !sorted {
1163                    top_k.sort_by_key(|(idx, _)| *idx);
1164                }
1165
1166                for (local_idx, val) in &top_k {
1167                    values_data.push(*val);
1168                    indices_data.push(*local_idx as i64);
1169                }
1170            }
1171        }
1172
1173        // Re-arrange from (outer, k, inner) to match result_shape layout
1174        // Currently we have data as outer * inner * k interleaved; need outer * k * inner
1175        // Transpose inner and k dimensions
1176        let transposed_len = outer_size * effective_k * inner_size;
1177        let mut values_transposed = Vec::with_capacity(transposed_len);
1178        let mut indices_transposed = Vec::with_capacity(transposed_len);
1179
1180        for outer in 0..outer_size {
1181            for k_idx in 0..effective_k {
1182                for inner in 0..inner_size {
1183                    let src = outer * inner_size * effective_k + inner * effective_k + k_idx;
1184                    values_transposed.push(values_data[src]);
1185                    indices_transposed.push(indices_data[src]);
1186                }
1187            }
1188        }
1189
1190        let values_tensor = Self::from_data(values_transposed, result_shape.clone(), self.device)?;
1191        let indices_tensor =
1192            Tensor::<i64>::from_data(indices_transposed, result_shape, self.device)?;
1193
1194        Ok((values_tensor, indices_tensor))
1195    }
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200    use super::*;
1201    use torsh_core::device::DeviceType;
1202
1203    /// Promoting `SimdOptimized` storage must produce a private buffer, so a
1204    /// write through one handle is never visible through a sibling clone.
1205    ///
1206    /// The promotion now copies straight out of the SIMD storage instead of
1207    /// going through an intermediate `Vec`; this test pins the isolation that
1208    /// the shortcut must not lose.
1209    #[cfg(feature = "simd")]
1210    #[test]
1211    fn make_unique_isolates_shared_simd_storage() {
1212        // 4096 f32 = 16 KiB, above the 10 KiB SimdOptimized threshold.
1213        let mut tensor = Tensor::from_data(vec![1.0f32; 4096], vec![4096], DeviceType::Cpu)
1214            .expect("tensor creation should succeed");
1215        assert!(
1216            matches!(tensor.storage, TensorStorage::SimdOptimized(_)),
1217            "precondition: a 16 KiB tensor uses lock-free SIMD storage"
1218        );
1219        let sibling = tensor.clone();
1220
1221        tensor.make_unique().expect("make_unique should succeed");
1222        assert!(
1223            !matches!(tensor.storage, TensorStorage::SimdOptimized(_)),
1224            "SimdOptimized storage must be promoted to single-buffer mutable storage"
1225        );
1226
1227        tensor.apply_(|value| value + 1.0).expect("apply_");
1228        assert_eq!(
1229            tensor.to_vec().expect("to_vec")[0],
1230            2.0,
1231            "the writer must see its own update"
1232        );
1233        assert_eq!(
1234            sibling.to_vec().expect("to_vec")[0],
1235            1.0,
1236            "the sibling must not observe the write"
1237        );
1238    }
1239
1240    /// The single-copy promotion must reproduce the data exactly, including for
1241    /// storage that had already been written through its copy-on-write path.
1242    #[cfg(feature = "simd")]
1243    #[test]
1244    fn make_unique_preserves_mutated_simd_contents() {
1245        let source = Tensor::from_data(
1246            (0..4096).map(|i| i as f32).collect::<Vec<_>>(),
1247            vec![4096],
1248            DeviceType::Cpu,
1249        )
1250        .expect("tensor creation should succeed");
1251
1252        // Write through the SIMD storage's copy-on-write path first.
1253        source
1254            .storage
1255            .with_slice_mut(|slice| {
1256                slice[0] = -1.0;
1257                Ok(())
1258            })
1259            .expect("copy-on-write write should succeed");
1260
1261        let mut promoted = source.clone();
1262        promoted.make_unique().expect("make_unique should succeed");
1263        let data = promoted.to_vec().expect("to_vec");
1264        assert_eq!(data[0], -1.0, "the promotion must copy the CoW buffer");
1265        assert_eq!(data[4095], 4095.0);
1266    }
1267
1268    #[test]
1269    fn test_scalar_creation() {
1270        let scalar = Tensor::<f32>::scalar(42.0).expect("operation should succeed");
1271        assert_eq!(scalar.shape().dims(), &[] as &[usize]);
1272        assert_eq!(scalar.item().expect("item extraction should succeed"), 42.0);
1273    }
1274
1275    #[test]
1276    fn test_max_reduction() {
1277        let data = vec![1.0f32, 5.0, 3.0, 2.0];
1278        let tensor =
1279            Tensor::from_data(data, vec![4], DeviceType::Cpu).expect("operation should succeed");
1280        let max_val = tensor.max(None, false).expect("operation should succeed");
1281        assert_eq!(max_val.item().expect("item extraction should succeed"), 5.0);
1282    }
1283
1284    #[test]
1285    fn test_norm_computation() {
1286        let data = vec![3.0f32, 4.0]; // 3-4-5 triangle
1287        let tensor =
1288            Tensor::from_data(data, vec![2], DeviceType::Cpu).expect("operation should succeed");
1289        let norm = tensor.norm().expect("norm computation should succeed");
1290        assert!((norm.item().expect("item extraction should succeed") - 5.0).abs() < 1e-6);
1291    }
1292
1293    #[test]
1294    fn test_apply_operations() {
1295        let data = vec![1.0f32, 2.0, 3.0, 4.0];
1296        let mut tensor =
1297            Tensor::from_data(data, vec![4], DeviceType::Cpu).expect("operation should succeed");
1298
1299        // Test apply_
1300        tensor
1301            .apply_(|x| x * 2.0)
1302            .expect("operation should succeed");
1303        assert_eq!(
1304            tensor.data().expect("data retrieval should succeed"),
1305            vec![2.0, 4.0, 6.0, 8.0]
1306        );
1307
1308        // Test map
1309        let original = Tensor::from_data(vec![1.0f32, 2.0, 3.0], vec![3], DeviceType::Cpu)
1310            .expect("operation should succeed");
1311        let mapped = original.map(|x| x + 1.0).expect("operation should succeed");
1312        assert_eq!(
1313            mapped.data().expect("data retrieval should succeed"),
1314            vec![2.0, 3.0, 4.0]
1315        );
1316        assert_eq!(
1317            original.data().expect("data retrieval should succeed"),
1318            vec![1.0, 2.0, 3.0]
1319        ); // Original unchanged
1320    }
1321
1322    #[test]
1323    fn test_activation_functions() {
1324        let data = vec![-1.0f32, 0.0, 1.0, 2.0];
1325        let tensor =
1326            Tensor::from_data(data, vec![4], DeviceType::Cpu).expect("operation should succeed");
1327
1328        // Test ReLU
1329        let relu_result = tensor.relu().expect("relu should succeed");
1330        assert_eq!(
1331            relu_result.data().expect("data retrieval should succeed"),
1332            vec![0.0, 0.0, 1.0, 2.0]
1333        );
1334
1335        // Test abs
1336        let abs_result = tensor.abs().expect("abs computation should succeed");
1337        assert_eq!(
1338            abs_result.data().expect("data retrieval should succeed"),
1339            vec![1.0, 0.0, 1.0, 2.0]
1340        );
1341
1342        // Test clamp
1343        let clamped = tensor.clamp(-0.5, 1.5).expect("operation should succeed");
1344        assert_eq!(
1345            clamped.data().expect("data retrieval should succeed"),
1346            vec![-0.5, 0.0, 1.0, 1.5]
1347        );
1348    }
1349
1350    #[test]
1351    fn test_storage_sharing() {
1352        let tensor1 =
1353            Tensor::<f32>::zeros(&[2, 2], DeviceType::Cpu).expect("operation should succeed");
1354        let tensor2 = tensor1.clone();
1355        let tensor3 = tensor1.clone_data();
1356
1357        assert!(tensor1.shares_storage(&tensor2));
1358        assert!(!tensor1.shares_storage(&tensor3));
1359    }
1360
1361    #[test]
1362    fn test_basic_matmul() {
1363        let a = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1364            .expect("operation should succeed");
1365        let b = Tensor::from_data(vec![5.0f32, 6.0, 7.0, 8.0], vec![2, 2], DeviceType::Cpu)
1366            .expect("operation should succeed");
1367
1368        let result = a.basic_matmul(&b).expect("operation should succeed");
1369        assert_eq!(result.shape().dims(), &[2, 2]);
1370
1371        // Expected: [1*5+2*7, 1*6+2*8] = [19, 22]
1372        //           [3*5+4*7, 3*6+4*8] = [43, 50]
1373        let expected = vec![19.0, 22.0, 43.0, 50.0];
1374        assert_eq!(
1375            result.data().expect("data retrieval should succeed"),
1376            expected
1377        );
1378    }
1379
1380    #[test]
1381    fn test_reductions() {
1382        let data = vec![1.0f32, 2.0, 3.0, 4.0];
1383        let tensor =
1384            Tensor::from_data(data, vec![4], DeviceType::Cpu).expect("operation should succeed");
1385
1386        let sum = tensor.sum().expect("sum should succeed");
1387        assert_eq!(sum.item().expect("item extraction should succeed"), 10.0);
1388
1389        let mean = tensor.mean(None, false).expect("operation should succeed");
1390        assert_eq!(mean.item().expect("item extraction should succeed"), 2.5);
1391    }
1392
1393    #[test]
1394    fn test_copy_on_write() {
1395        let mut tensor1 =
1396            Tensor::<f32>::ones(&[2], DeviceType::Cpu).expect("operation should succeed");
1397        let tensor2 = tensor1.clone();
1398
1399        // Both should share storage initially
1400        assert!(tensor1.shares_storage(&tensor2));
1401
1402        // After make_unique, they should not share storage
1403        tensor1.make_unique().expect("make_unique should succeed");
1404        assert!(!tensor1.shares_storage(&tensor2));
1405    }
1406
1407    /// F171: `make_unique` on an unshared tensor must not keep reallocating.
1408    ///
1409    /// `create_optimal` hands every f32 tensor >= 2560 elements the *immutable*
1410    /// `SimdOptimized` storage, so the first call has to convert it once to
1411    /// mutable `Aligned` storage. Every later call must be free.
1412    #[test]
1413    fn test_make_unique_does_not_realloc_unshared_storage() {
1414        let mut tensor = Tensor::<f32>::from_data(vec![1.0; 4096], vec![4096], DeviceType::Cpu)
1415            .expect("tensor creation should succeed");
1416        assert_eq!(
1417            tensor.storage_type(),
1418            "simd_optimized",
1419            "precondition: large f32 tensors start out immutable"
1420        );
1421
1422        tensor.make_unique().expect("make_unique should succeed");
1423        assert_eq!(tensor.storage_type(), "aligned_simd");
1424        let ptr_after_first = tensor
1425            .storage
1426            .with_slice(|s| Ok(s.as_ptr() as usize))
1427            .expect("with_slice should succeed");
1428
1429        tensor.make_unique().expect("make_unique should succeed");
1430        let ptr_after_second = tensor
1431            .storage
1432            .with_slice(|s| Ok(s.as_ptr() as usize))
1433            .expect("with_slice should succeed");
1434
1435        assert_eq!(
1436            ptr_after_first, ptr_after_second,
1437            "make_unique must not copy storage that is already unique and mutable"
1438        );
1439    }
1440
1441    /// F171: the copy-on-write copy must stay *mutable*.
1442    ///
1443    /// `create_optimal` would hand a large f32 tensor immutable `SimdOptimized`
1444    /// storage again, so every in-place op after a CoW split would fail.
1445    #[test]
1446    fn test_make_unique_cow_copy_stays_mutable() {
1447        let mut tensor = Tensor::<f32>::from_data(vec![1.0; 4096], vec![4096], DeviceType::Cpu)
1448            .expect("tensor creation should succeed");
1449        tensor.make_unique().expect("make_unique should succeed");
1450        assert_eq!(tensor.storage_type(), "aligned_simd");
1451
1452        let shared = tensor.clone();
1453        tensor.make_unique().expect("make_unique should succeed");
1454        assert_eq!(
1455            tensor.storage_type(),
1456            "aligned_simd",
1457            "a copy-on-write split must not fall back to immutable storage"
1458        );
1459        tensor.apply_(|x| x + 1.0).expect("apply_ should succeed");
1460        assert_eq!(tensor.to_vec().expect("to_vec")[0], 2.0);
1461        assert_eq!(
1462            shared.to_vec().expect("to_vec")[0],
1463            1.0,
1464            "the shared tensor must be unaffected"
1465        );
1466    }
1467
1468    /// F058/F168: `apply_` mutates the existing buffer instead of replacing it.
1469    #[test]
1470    fn test_apply_mutates_storage_in_place() {
1471        let mut tensor = Tensor::<f32>::from_data(vec![2.0; 4096], vec![4096], DeviceType::Cpu)
1472            .expect("tensor creation should succeed");
1473        tensor.make_unique().expect("make_unique should succeed");
1474        let ptr_before = tensor
1475            .storage
1476            .with_slice(|s| Ok(s.as_ptr() as usize))
1477            .expect("with_slice should succeed");
1478
1479        tensor.apply_(|x| x * 3.0).expect("apply_ should succeed");
1480
1481        let ptr_after = tensor
1482            .storage
1483            .with_slice(|s| Ok(s.as_ptr() as usize))
1484            .expect("with_slice should succeed");
1485        assert_eq!(ptr_before, ptr_after, "apply_ must not reallocate storage");
1486        assert_eq!(tensor.to_vec().expect("to_vec")[0], 6.0);
1487    }
1488
1489    #[test]
1490    fn test_item_extraction() {
1491        let scalar = Tensor::from_data(vec![42.0f32], vec![], DeviceType::Cpu)
1492            .expect("operation should succeed");
1493        assert_eq!(scalar.item().expect("item extraction should succeed"), 42.0);
1494
1495        let vector = Tensor::from_data(vec![1.0f32, 2.0], vec![2], DeviceType::Cpu)
1496            .expect("operation should succeed");
1497        assert!(vector.item().is_err()); // Should fail for multi-element tensor
1498    }
1499
1500    #[test]
1501    fn test_all_dim() {
1502        // Shape [2, 3]: [[1, 0, 1], [1, 1, 1]]
1503        let data = vec![1i32, 0, 1, 1, 1, 1];
1504        let tensor = Tensor::from_data(data, vec![2, 3], DeviceType::Cpu)
1505            .expect("tensor creation should succeed");
1506
1507        // all along dim 0 (rows): per column check
1508        // col0: 1&&1=true, col1: 0&&1=false, col2: 1&&1=true
1509        let result = tensor.all_dim(0, false).expect("all_dim should succeed");
1510        assert_eq!(result.shape().dims(), &[3]);
1511        assert_eq!(
1512            result.to_vec().expect("to_vec should succeed"),
1513            vec![true, false, true]
1514        );
1515
1516        // all along dim 1 (cols): per row check
1517        // row0: 1&&0&&1=false, row1: 1&&1&&1=true
1518        let result_row = tensor.all_dim(1, false).expect("all_dim should succeed");
1519        assert_eq!(result_row.shape().dims(), &[2]);
1520        assert_eq!(
1521            result_row.to_vec().expect("to_vec should succeed"),
1522            vec![false, true]
1523        );
1524
1525        // keepdim=true preserves dimension
1526        let result_kd = tensor.all_dim(1, true).expect("all_dim should succeed");
1527        assert_eq!(result_kd.shape().dims(), &[2, 1]);
1528    }
1529
1530    #[test]
1531    fn test_any_dim() {
1532        // Shape [2, 3]: [[0, 0, 0], [0, 1, 0]]
1533        let data = vec![0i32, 0, 0, 0, 1, 0];
1534        let tensor = Tensor::from_data(data, vec![2, 3], DeviceType::Cpu)
1535            .expect("tensor creation should succeed");
1536
1537        // any along dim 0: col0: false, col1: true, col2: false
1538        let result = tensor.any_dim(0, false).expect("any_dim should succeed");
1539        assert_eq!(result.shape().dims(), &[3]);
1540        assert_eq!(
1541            result.to_vec().expect("to_vec should succeed"),
1542            vec![false, true, false]
1543        );
1544
1545        // any along dim 1: row0: false, row1: true
1546        let result_row = tensor.any_dim(1, false).expect("any_dim should succeed");
1547        assert_eq!(result_row.shape().dims(), &[2]);
1548        assert_eq!(
1549            result_row.to_vec().expect("to_vec should succeed"),
1550            vec![false, true]
1551        );
1552    }
1553
1554    #[test]
1555    fn test_cat_multidim() {
1556        // Test concatenation along dim 0
1557        let a = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1558            .expect("tensor creation should succeed");
1559        let b = Tensor::from_data(vec![5.0f32, 6.0], vec![1, 2], DeviceType::Cpu)
1560            .expect("tensor creation should succeed");
1561
1562        let cat0 = Tensor::<f32>::cat(&[&a, &b], 0).expect("cat should succeed");
1563        assert_eq!(cat0.shape().dims(), &[3, 2]);
1564        assert_eq!(
1565            cat0.to_vec().expect("to_vec should succeed"),
1566            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
1567        );
1568
1569        // Test concatenation along dim 1
1570        let c = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1571            .expect("tensor creation should succeed");
1572        let d = Tensor::from_data(vec![5.0f32, 6.0, 7.0, 8.0], vec![2, 2], DeviceType::Cpu)
1573            .expect("tensor creation should succeed");
1574
1575        let cat1 = Tensor::<f32>::cat(&[&c, &d], 1).expect("cat should succeed");
1576        assert_eq!(cat1.shape().dims(), &[2, 4]);
1577        assert_eq!(
1578            cat1.to_vec().expect("to_vec should succeed"),
1579            vec![1.0, 2.0, 5.0, 6.0, 3.0, 4.0, 7.0, 8.0]
1580        );
1581    }
1582
1583    #[test]
1584    fn test_topk_along_dim() {
1585        // 2x4 tensor, topk along dim 1
1586        let data = vec![3.0f32, 1.0, 4.0, 2.0, 5.0, 9.0, 2.0, 6.0];
1587        let tensor = Tensor::from_data(data, vec![2, 4], DeviceType::Cpu)
1588            .expect("tensor creation should succeed");
1589
1590        let (vals, idxs) = tensor
1591            .topk(2, Some(1), true, true)
1592            .expect("topk should succeed");
1593        assert_eq!(vals.shape().dims(), &[2, 2]);
1594        assert_eq!(idxs.shape().dims(), &[2, 2]);
1595
1596        // Row 0: [3, 1, 4, 2] -> top2 = [4, 3] at positions [2, 0]
1597        // Row 1: [5, 9, 2, 6] -> top2 = [9, 6] at positions [1, 3]
1598        let vals_data = vals.to_vec().expect("to_vec should succeed");
1599        let idxs_data = idxs.to_vec().expect("to_vec should succeed");
1600        assert_eq!(vals_data[0], 4.0);
1601        assert_eq!(vals_data[1], 3.0);
1602        assert_eq!(vals_data[2], 9.0);
1603        assert_eq!(vals_data[3], 6.0);
1604        assert_eq!(idxs_data[0], 2);
1605        assert_eq!(idxs_data[1], 0);
1606        assert_eq!(idxs_data[2], 1);
1607        assert_eq!(idxs_data[3], 3);
1608    }
1609
1610    // --- Regression tests for issue #43: mean must propagate requires_grad ---
1611
1612    #[test]
1613    fn test_issue_43_mean_propagates_requires_grad() {
1614        // A tensor with requires_grad=true; mean result must also require grad.
1615        let input = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu)
1616            .expect("tensor creation failed")
1617            .requires_grad_(true);
1618
1619        let result = input.mean(None, false).expect("mean should succeed");
1620        assert!(
1621            result.requires_grad(),
1622            "mean result must have requires_grad=true when input does"
1623        );
1624    }
1625
1626    #[test]
1627    fn test_issue_43_mean_no_requires_grad_when_input_has_none() {
1628        // When input does not require grad, result should not either.
1629        let input = Tensor::from_data(vec![1.0f32, 2.0, 3.0], vec![3], DeviceType::Cpu)
1630            .expect("tensor creation failed");
1631
1632        let result = input.mean(None, false).expect("mean should succeed");
1633        assert!(
1634            !result.requires_grad(),
1635            "mean result must not require grad when input does not"
1636        );
1637    }
1638
1639    #[test]
1640    fn test_issue_43_mean_backward() {
1641        // For mean of n elements, backward with upstream grad=1 distributes 1/n to each element.
1642        // mean() already reduces to a scalar, so backward() can be called directly.
1643        let n = 4usize;
1644        let input = Tensor::from_data(vec![2.0f32, 4.0, 6.0, 8.0], vec![n], DeviceType::Cpu)
1645            .expect("tensor creation failed")
1646            .requires_grad_(true);
1647
1648        let result = input.mean(None, false).expect("mean should succeed");
1649        assert!(result.requires_grad(), "mean result must track gradients");
1650        // mean(None) with keepdim=false produces a scalar (numel=1), so backward is valid
1651        result.backward().expect("backward should succeed");
1652
1653        let grad = input
1654            .grad()
1655            .expect("input must have gradient after backward");
1656        let grad_data = grad.data().expect("gradient data");
1657
1658        // Each element should receive 1.0 / n = 0.25
1659        let expected = 1.0f32 / n as f32;
1660        for &g in &grad_data {
1661            assert!(
1662                (g - expected).abs() < 1e-6,
1663                "each element grad should be 1/n={expected}, got {g}"
1664            );
1665        }
1666    }
1667
1668    #[test]
1669    fn test_sum_backward() {
1670        // loss = sum(x); d(loss)/dx_i = 1 for every element.
1671        let x = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu)
1672            .expect("tensor creation failed")
1673            .requires_grad_(true);
1674        let loss = x.sum().expect("sum should succeed");
1675        assert!(loss.requires_grad(), "sum result must track gradients");
1676        loss.backward().expect("backward should succeed");
1677        let grad = x.grad().expect("x must have a gradient after backward");
1678        let grad_data = grad.data().expect("gradient data");
1679        assert_eq!(
1680            grad_data,
1681            vec![1.0f32, 1.0, 1.0, 1.0],
1682            "d(sum)/dx must be all ones"
1683        );
1684    }
1685
1686    #[test]
1687    fn test_matmul_backward() {
1688        // C = A @ B, loss = sum(C). grad_C = ones, so:
1689        //   grad_A = ones @ Bᵀ = [[11,15],[11,15]]
1690        //   grad_B = Aᵀ @ ones = [[4,4],[6,6]]
1691        // for A = [[1,2],[3,4]], B = [[5,6],[7,8]].
1692        let a = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1693            .expect("tensor creation failed")
1694            .requires_grad_(true);
1695        let b = Tensor::from_data(vec![5.0f32, 6.0, 7.0, 8.0], vec![2, 2], DeviceType::Cpu)
1696            .expect("tensor creation failed")
1697            .requires_grad_(true);
1698
1699        let c = a.matmul(&b).expect("matmul should succeed");
1700        assert!(c.requires_grad(), "matmul result must track gradients");
1701        let loss = c.sum().expect("sum should succeed");
1702        loss.backward().expect("backward should succeed");
1703
1704        let grad_a = a
1705            .grad()
1706            .expect("A must have a gradient")
1707            .data()
1708            .expect("grad data");
1709        let grad_b = b
1710            .grad()
1711            .expect("B must have a gradient")
1712            .data()
1713            .expect("grad data");
1714        assert_eq!(
1715            grad_a,
1716            vec![11.0f32, 15.0, 11.0, 15.0],
1717            "grad_A = ones @ Bᵀ"
1718        );
1719        assert_eq!(grad_b, vec![4.0f32, 4.0, 6.0, 6.0], "grad_B = Aᵀ @ ones");
1720    }
1721}