Skip to main content

tensor4all_tensorbackend/
storage.rs

1use anyhow::{anyhow, ensure, Result};
2use num_complex::Complex64;
3use std::ops::Mul;
4use std::sync::Arc;
5
6/// Trait for scalar types that can be stored in [`Storage`].
7///
8/// This enables generic constructors such as [`Storage::from_dense_col_major`]
9/// and [`Storage::from_diag_col_major`]. Implemented for `f64` and `Complex64`.
10///
11/// # Examples
12///
13/// ```
14/// use tensor4all_tensorbackend::{Storage, StorageScalar};
15///
16/// // Using the generic constructor -- scalar type is inferred from data
17/// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0], &[3]).unwrap();
18/// assert!(s.is_f64());
19///
20/// use num_complex::Complex64;
21/// let c = Storage::from_dense_col_major(
22///     vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)],
23///     &[2],
24/// ).unwrap();
25/// assert!(c.is_c64());
26/// ```
27pub trait StorageScalar: Clone + Send + Sync + 'static {
28    /// Build a dense [`Storage`] from column-major data.
29    /// # Errors
30    ///
31    /// Returns an error when the data length does not match the logical dimension
32    /// /// product (a shape mismatch).
33    ///
34    fn build_dense_storage(data: Vec<Self>, logical_dims: &[usize]) -> Result<Storage>;
35    /// Build a diagonal [`Storage`] from diagonal payload data.
36    /// # Errors
37    ///
38    /// Returns an error when the diagonal payload is incompatible with the logical
39    /// /// rank (a shape mismatch).
40    ///
41    fn build_diag_storage(diag_data: Vec<Self>, logical_rank: usize) -> Result<Storage>;
42    /// Build a structured [`Storage`] from explicit payload metadata.
43    /// # Errors
44    ///
45    /// Returns an error when the structured metadata is inconsistent (an
46    /// /// invalid-storage failure).
47    ///
48    fn build_structured_storage(
49        data: Vec<Self>,
50        payload_dims: Vec<usize>,
51        strides: Vec<isize>,
52        axis_classes: Vec<usize>,
53    ) -> Result<Storage>;
54}
55
56impl StorageScalar for f64 {
57    fn build_dense_storage(data: Vec<Self>, logical_dims: &[usize]) -> Result<Storage> {
58        Storage::validate_dense_len(&data, logical_dims, "dense f64 payload")?;
59        Ok(Storage::from_repr(StorageRepr::F64(
60            StructuredStorage::from_dense_col_major(data, logical_dims)?,
61        )))
62    }
63    fn build_diag_storage(diag_data: Vec<Self>, logical_rank: usize) -> Result<Storage> {
64        Ok(Storage::from_repr(StorageRepr::F64(
65            StructuredStorage::from_diag_col_major(diag_data, logical_rank)?,
66        )))
67    }
68    fn build_structured_storage(
69        data: Vec<Self>,
70        payload_dims: Vec<usize>,
71        strides: Vec<isize>,
72        axis_classes: Vec<usize>,
73    ) -> Result<Storage> {
74        Ok(Storage::from_repr(StorageRepr::F64(
75            StructuredStorage::new(data, payload_dims, strides, axis_classes)?,
76        )))
77    }
78}
79
80impl StorageScalar for Complex64 {
81    fn build_dense_storage(data: Vec<Self>, logical_dims: &[usize]) -> Result<Storage> {
82        Storage::validate_dense_len(&data, logical_dims, "dense c64 payload")?;
83        Ok(Storage::from_repr(StorageRepr::C64(
84            StructuredStorage::from_dense_col_major(data, logical_dims)?,
85        )))
86    }
87    fn build_diag_storage(diag_data: Vec<Self>, logical_rank: usize) -> Result<Storage> {
88        Ok(Storage::from_repr(StorageRepr::C64(
89            StructuredStorage::from_diag_col_major(diag_data, logical_rank)?,
90        )))
91    }
92    fn build_structured_storage(
93        data: Vec<Self>,
94        payload_dims: Vec<usize>,
95        strides: Vec<isize>,
96        axis_classes: Vec<usize>,
97    ) -> Result<Storage> {
98        Ok(Storage::from_repr(StorageRepr::C64(
99            StructuredStorage::new(data, payload_dims, strides, axis_classes)?,
100        )))
101    }
102}
103
104pub(crate) fn col_major_strides(dims: &[usize]) -> Result<Vec<isize>> {
105    let mut strides = Vec::with_capacity(dims.len());
106    let mut stride = 1isize;
107    for &dim in dims {
108        strides.push(stride);
109        let dim = isize::try_from(dim)
110            .map_err(|_| anyhow!("column-major stride overflow for dims {dims:?}"))?;
111        stride = stride
112            .checked_mul(dim)
113            .ok_or_else(|| anyhow!("column-major stride overflow for dims {dims:?}"))?;
114    }
115    Ok(strides)
116}
117
118fn validate_canonical_axis_classes(axis_classes: &[usize]) -> Result<()> {
119    let mut next_class = 0usize;
120    for &class_id in axis_classes {
121        ensure!(
122            class_id <= next_class,
123            "axis_classes must be canonical first-appearance labels, got {axis_classes:?}"
124        );
125        if class_id == next_class {
126            next_class = next_class
127                .checked_add(1)
128                .ok_or_else(|| anyhow!("axis class index overflow"))?;
129        }
130    }
131    Ok(())
132}
133
134fn required_storage_len(dims: &[usize], strides: &[isize]) -> Result<usize> {
135    ensure!(
136        dims.len() == strides.len(),
137        "payload dims {:?} and strides {:?} must have the same rank",
138        dims,
139        strides
140    );
141
142    // Validate the compact payload product even when a zero stride would make
143    // the referenced backing span look small. Iteration must never begin with
144    // a wrapped payload length.
145    let payload_len = dims.iter().try_fold(1usize, |length, &dim| {
146        length
147            .checked_mul(dim)
148            .ok_or_else(|| anyhow!("payload length overflow for dims {dims:?}"))
149    })?;
150
151    let mut max_offset = 0usize;
152    for (&dim, &stride) in dims.iter().zip(strides.iter()) {
153        ensure!(
154            stride >= 0,
155            "negative strides are not supported in StructuredStorage: {strides:?}"
156        );
157        let stride = usize::try_from(stride)
158            .map_err(|_| anyhow!("payload stride overflow for dims {dims:?}"))?;
159        if dim > 1 {
160            let span = (dim - 1)
161                .checked_mul(stride)
162                .ok_or_else(|| anyhow!("payload stride overflow for dims {dims:?}"))?;
163            max_offset = max_offset
164                .checked_add(span)
165                .ok_or_else(|| anyhow!("payload stride overflow for dims {dims:?}"))?;
166        }
167    }
168    if payload_len == 0 {
169        return Ok(0);
170    }
171    max_offset
172        .checked_add(1)
173        .ok_or_else(|| anyhow!("payload stride overflow for dims {dims:?}"))
174}
175
176fn logical_dims_from_axis_classes(payload_dims: &[usize], axis_classes: &[usize]) -> Vec<usize> {
177    axis_classes
178        .iter()
179        .map(|&class_id| payload_dims[class_id])
180        .collect()
181}
182
183fn col_major_multi_index(mut linear: usize, dims: &[usize]) -> Vec<usize> {
184    let mut index = Vec::with_capacity(dims.len());
185    for &dim in dims {
186        if dim == 0 {
187            index.push(0);
188        } else {
189            index.push(linear % dim);
190            linear /= dim;
191        }
192    }
193    index
194}
195
196fn offset_from_strides(index: &[usize], strides: &[isize]) -> Option<usize> {
197    index
198        .iter()
199        .zip(strides.iter())
200        .try_fold(0usize, |offset, (&value, &stride)| {
201            let stride = usize::try_from(stride).ok()?;
202            let term = value.checked_mul(stride)?;
203            offset.checked_add(term)
204        })
205}
206
207/// Structured tensor snapshot storage.
208///
209/// `data` and `strides` describe the payload tensor, while `axis_classes`
210/// describes how logical axes map onto payload axes. Logical flat-buffer
211/// semantics are column-major. A strided payload may contain unused backing
212/// gaps; reductions and nonfinite scans visit only entries addressed by the
213/// payload dimensions and strides.
214///
215/// A **dense** tensor has `axis_classes = [0, 1, ..., rank-1]` (each logical
216/// axis maps to a distinct payload axis). A **diagonal** tensor has
217/// `axis_classes = [0, 0, ..., 0]` (all logical axes share one payload axis),
218/// storing only the diagonal entries.
219///
220/// # Examples
221///
222/// ```
223/// use tensor4all_tensorbackend::StructuredStorage;
224///
225/// // Dense 2x3 storage, column-major: [[1,3,5],[2,4,6]]
226/// let dense = StructuredStorage::from_dense_col_major(
227///     vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3],
228/// ).unwrap();
229/// assert!(dense.is_dense());
230/// assert!(!dense.is_diag());
231/// assert_eq!(dense.logical_rank(), 2);
232/// assert_eq!(dense.logical_dims(), vec![2, 3]);
233///
234/// // Diagonal 3x3 storage
235/// let diag = StructuredStorage::from_diag_col_major(vec![1.0, 2.0, 3.0], 2).unwrap();
236/// assert!(diag.is_diag());
237/// assert_eq!(diag.logical_dims(), vec![3, 3]);
238/// assert_eq!(diag.len(), 3);
239/// ```
240#[derive(Debug, Clone, PartialEq)]
241pub struct StructuredStorage<T> {
242    data: Vec<T>,
243    payload_dims: Vec<usize>,
244    strides: Vec<isize>,
245    axis_classes: Vec<usize>,
246}
247
248impl<T> StructuredStorage<T> {
249    /// Creates a structured payload snapshot from explicit payload metadata.
250    ///
251    /// `payload_dims` and `strides` describe the compressed payload tensor,
252    /// while `axis_classes` maps logical axes onto payload axes in canonical
253    /// first-appearance order.
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if:
258    /// - `axis_classes` is not in canonical first-appearance form
259    /// - `payload_dims` rank does not match `axis_classes`
260    /// - `strides` rank does not match `payload_dims`
261    /// - `data` length does not match the required storage length
262    ///
263    /// # Examples
264    ///
265    /// ```
266    /// use tensor4all_tensorbackend::StructuredStorage;
267    ///
268    /// // Dense 2x3 with explicit column-major strides
269    /// let s = StructuredStorage::new(
270    ///     vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
271    ///     vec![2, 3],     // payload_dims
272    ///     vec![1, 2],     // column-major strides
273    ///     vec![0, 1],     // axis_classes: each axis is independent
274    /// ).unwrap();
275    /// assert!(s.is_dense());
276    /// assert_eq!(s.len(), 6);
277    /// ```
278    pub fn new(
279        data: Vec<T>,
280        payload_dims: Vec<usize>,
281        strides: Vec<isize>,
282        axis_classes: Vec<usize>,
283    ) -> Result<Self> {
284        let required_len =
285            Storage::validate_structured_metadata(&payload_dims, &strides, &axis_classes)?;
286        ensure!(
287            data.len() == required_len,
288            "payload storage len {} does not match required len {} for dims {:?} and strides {:?}",
289            data.len(),
290            required_len,
291            payload_dims,
292            strides
293        );
294        Ok(Self {
295            data,
296            payload_dims,
297            strides,
298            axis_classes,
299        })
300    }
301
302    /// Creates a dense structured snapshot from column-major logical data.
303    ///
304    /// # Errors
305    ///
306    /// Returns an error when the data length does not match the logical dimension
307    /// /// product (a shape mismatch).
308    ///
309    /// # Examples
310    ///
311    /// ```
312    /// use tensor4all_tensorbackend::StructuredStorage;
313    ///
314    /// let s = StructuredStorage::from_dense_col_major(vec![10.0, 20.0, 30.0, 40.0], &[2, 2]).unwrap();
315    /// assert!(s.is_dense());
316    /// assert_eq!(s.data(), &[10.0, 20.0, 30.0, 40.0]);
317    /// ```
318    pub fn from_dense_col_major(data: Vec<T>, logical_dims: &[usize]) -> StorageResult<Self> {
319        let payload_dims = logical_dims.to_vec();
320        let strides = col_major_strides(&payload_dims)?;
321        let axis_classes = (0..logical_dims.len()).collect();
322        Self::new(data, payload_dims, strides, axis_classes).map_err(StorageError::from)
323    }
324
325    /// Creates a diagonal structured snapshot from column-major diagonal data.
326    ///
327    /// The resulting tensor has `logical_rank` axes, each of size `diag_data.len()`.
328    /// Only the diagonal entries are stored.
329    ///
330    /// # Errors
331    ///
332    /// Returns an error when the diagonal payload is incompatible with the logical
333    /// /// rank (a shape mismatch).
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// use tensor4all_tensorbackend::StructuredStorage;
339    ///
340    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0, 3.0], 2).unwrap();
341    /// assert!(d.is_diag());
342    /// assert_eq!(d.logical_dims(), vec![3, 3]);
343    /// assert_eq!(d.data(), &[1.0, 2.0, 3.0]);
344    /// ```
345    pub fn from_diag_col_major(diag_data: Vec<T>, logical_rank: usize) -> StorageResult<Self> {
346        let payload_dims = if logical_rank == 0 {
347            vec![]
348        } else {
349            vec![diag_data.len()]
350        };
351        let strides = col_major_strides(&payload_dims)?;
352        let axis_classes = vec![0; logical_rank];
353        Self::new(diag_data, payload_dims, strides, axis_classes).map_err(StorageError::from)
354    }
355
356    /// Returns the payload data buffer as a slice.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use tensor4all_tensorbackend::StructuredStorage;
362    ///
363    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0], &[2]).unwrap();
364    /// assert_eq!(s.data(), &[1.0, 2.0]);
365    /// ```
366    pub fn data(&self) -> &[T] {
367        &self.data
368    }
369
370    /// Returns the payload tensor dimensions.
371    ///
372    /// For dense tensors, this equals the logical dimensions. For diagonal
373    /// tensors, this is a single-element slice `[n]` where `n` is the diagonal
374    /// length.
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use tensor4all_tensorbackend::StructuredStorage;
380    ///
381    /// let s = StructuredStorage::from_dense_col_major(vec![0.0; 6], &[2, 3]).unwrap();
382    /// assert_eq!(s.payload_dims(), &[2, 3]);
383    ///
384    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 3).unwrap();
385    /// assert_eq!(d.payload_dims(), &[2]);
386    /// ```
387    pub fn payload_dims(&self) -> &[usize] {
388        &self.payload_dims
389    }
390
391    /// Returns the payload tensor strides.
392    ///
393    /// # Examples
394    ///
395    /// ```
396    /// use tensor4all_tensorbackend::StructuredStorage;
397    ///
398    /// // Column-major 2x3: strides are [1, 2]
399    /// let s = StructuredStorage::from_dense_col_major(vec![0.0; 6], &[2, 3]).unwrap();
400    /// assert_eq!(s.strides(), &[1, 2]);
401    /// ```
402    pub fn strides(&self) -> &[isize] {
403        &self.strides
404    }
405
406    /// Returns the canonical logical-to-payload axis classes.
407    ///
408    /// Each entry maps a logical axis to a payload axis index. Repeated values
409    /// indicate axes that share the same payload dimension (e.g., diagonal).
410    ///
411    /// # Examples
412    ///
413    /// ```
414    /// use tensor4all_tensorbackend::StructuredStorage;
415    ///
416    /// let dense = StructuredStorage::from_dense_col_major(vec![0.0; 4], &[2, 2]).unwrap();
417    /// assert_eq!(dense.axis_classes(), &[0, 1]);
418    ///
419    /// let diag = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
420    /// assert_eq!(diag.axis_classes(), &[0, 0]);
421    /// ```
422    pub fn axis_classes(&self) -> &[usize] {
423        &self.axis_classes
424    }
425
426    /// Returns the logical dimensions derived from `payload_dims` and `axis_classes`.
427    ///
428    /// # Examples
429    ///
430    /// ```
431    /// use tensor4all_tensorbackend::StructuredStorage;
432    ///
433    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0, 3.0], 3).unwrap();
434    /// assert_eq!(d.logical_dims(), vec![3, 3, 3]);
435    /// ```
436    pub fn logical_dims(&self) -> Vec<usize> {
437        logical_dims_from_axis_classes(&self.payload_dims, &self.axis_classes)
438    }
439
440    /// Returns the logical rank (number of logical axes).
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// use tensor4all_tensorbackend::StructuredStorage;
446    ///
447    /// let s = StructuredStorage::from_dense_col_major(vec![0.0; 6], &[2, 3]).unwrap();
448    /// assert_eq!(s.logical_rank(), 2);
449    /// ```
450    pub fn logical_rank(&self) -> usize {
451        self.axis_classes.len()
452    }
453
454    /// Returns `true` when the logical tensor is dense (each logical axis maps
455    /// to a unique payload axis).
456    ///
457    /// # Examples
458    ///
459    /// ```
460    /// use tensor4all_tensorbackend::StructuredStorage;
461    ///
462    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0], &[2]).unwrap();
463    /// assert!(s.is_dense());
464    ///
465    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
466    /// assert!(!d.is_dense());
467    /// ```
468    pub fn is_dense(&self) -> bool {
469        self.axis_classes
470            .iter()
471            .copied()
472            .eq(0..self.axis_classes.len())
473    }
474
475    /// Returns `true` when the logical tensor is diagonal (rank >= 2 and all
476    /// logical axes map to the same payload axis).
477    ///
478    /// # Examples
479    ///
480    /// ```
481    /// use tensor4all_tensorbackend::StructuredStorage;
482    ///
483    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
484    /// assert!(d.is_diag());
485    ///
486    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0], &[2]).unwrap();
487    /// assert!(!s.is_diag());
488    /// ```
489    pub fn is_diag(&self) -> bool {
490        self.logical_rank() >= 2 && self.axis_classes.iter().all(|&class_id| class_id == 0)
491    }
492
493    /// Returns the payload buffer length.
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// use tensor4all_tensorbackend::StructuredStorage;
499    ///
500    /// let dense = StructuredStorage::from_dense_col_major(vec![1.0, 2.0, 3.0], &[3]).unwrap();
501    /// assert_eq!(dense.len(), 3);
502    ///
503    /// let diag = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
504    /// assert_eq!(diag.len(), 2);
505    /// ```
506    pub fn len(&self) -> usize {
507        self.data.len()
508    }
509
510    /// Returns `true` when the payload buffer is empty.
511    ///
512    /// # Examples
513    ///
514    /// ```
515    /// use tensor4all_tensorbackend::StructuredStorage;
516    ///
517    /// let empty = StructuredStorage::from_dense_col_major(Vec::<f64>::new(), &[0]).unwrap();
518    /// assert!(empty.is_empty());
519    ///
520    /// let non_empty = StructuredStorage::from_dense_col_major(vec![1.0], &[1]).unwrap();
521    /// assert!(!non_empty.is_empty());
522    /// ```
523    pub fn is_empty(&self) -> bool {
524        self.data.is_empty()
525    }
526
527    /// Returns a borrowed view when the logical tensor is dense and the
528    /// payload is already stored contiguously in column-major order.
529    ///
530    /// Returns `None` for diagonal or non-contiguous payloads.
531    ///
532    /// # Examples
533    ///
534    /// ```
535    /// use tensor4all_tensorbackend::StructuredStorage;
536    ///
537    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0, 3.0], &[3]).unwrap();
538    /// assert_eq!(s.dense_col_major_view_if_contiguous(), Some(&[1.0, 2.0, 3.0][..]));
539    ///
540    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
541    /// assert_eq!(d.dense_col_major_view_if_contiguous(), None);
542    /// ```
543    pub fn dense_col_major_view_if_contiguous(&self) -> Option<&[T]> {
544        if self.is_dense()
545            && matches!(col_major_strides(&self.payload_dims), Ok(strides) if strides == self.strides)
546        {
547            Some(&self.data)
548        } else {
549            None
550        }
551    }
552
553    /// Returns a borrowed compact-payload view when the payload is already
554    /// stored contiguously in column-major order.
555    pub fn payload_col_major_view_if_contiguous(&self) -> Option<&[T]> {
556        if matches!(col_major_strides(&self.payload_dims), Ok(strides) if strides == self.strides) {
557            Some(&self.data)
558        } else {
559            None
560        }
561    }
562}
563
564impl<T: Clone> StructuredStorage<T> {
565    /// Materializes the payload tensor as a contiguous column-major buffer.
566    ///
567    /// If the payload is already column-major, returns a clone.
568    ///
569    /// # Examples
570    ///
571    /// ```
572    /// use tensor4all_tensorbackend::StructuredStorage;
573    ///
574    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]).unwrap();
575    /// assert_eq!(s.payload_col_major_vec(), vec![1.0, 2.0, 3.0, 4.0]);
576    /// ```
577    pub fn payload_col_major_vec(&self) -> Vec<T> {
578        let payload_len = self
579            .payload_dims
580            .iter()
581            .try_fold(1usize, |length, &dim| length.checked_mul(dim))
582            // `StructuredStorage::new` validates this invariant before any
583            // instance can be constructed; return an empty vector rather than
584            // wrapping if an invalid value ever reaches this private state.
585            .unwrap_or(0);
586        if payload_len == 0 {
587            return Vec::new();
588        }
589        if matches!(col_major_strides(&self.payload_dims), Ok(strides) if strides == self.strides) {
590            return self.data.clone();
591        }
592
593        let mut output = Vec::with_capacity(payload_len);
594        for linear in 0..payload_len {
595            let index = col_major_multi_index(linear, &self.payload_dims);
596            let Some(offset) = offset_from_strides(&index, &self.strides) else {
597                return Vec::new();
598            };
599            output.push(self.data[offset].clone());
600        }
601        output
602    }
603
604    /// Returns a copy of the storage with logical axes permuted.
605    ///
606    /// # Errors
607    ///
608    /// Returns an error when the permutation is not a valid reordering of the axes
609    /// /// (an invalid-index failure).
610    ///
611    /// # Examples
612    ///
613    /// ```
614    /// use tensor4all_tensorbackend::StructuredStorage;
615    ///
616    /// // Diagonal 3x3x3 tensor; permute axes (identity for diag is always valid)
617    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0, 3.0], 3).unwrap();
618    /// let p = d.permute_logical_axes(&[2, 0, 1]).unwrap();
619    /// // Diagonal: all axes share the same dimension, so dims stay the same
620    /// assert_eq!(p.logical_dims(), vec![3, 3, 3]);
621    /// assert!(p.is_diag());
622    /// ```
623    pub fn permute_logical_axes(&self, perm: &[usize]) -> StorageResult<Self> {
624        if perm.len() != self.axis_classes.len() {
625            return Err(StorageError::from(anyhow::anyhow!(
626                "logical permutation length {} must match logical rank {}",
627                perm.len(),
628                self.axis_classes.len()
629            )));
630        }
631        let mut seen = vec![false; self.axis_classes.len()];
632        let axis_classes = perm
633            .iter()
634            .map(|&index| {
635                if index >= self.axis_classes.len() {
636                    return Err(anyhow::anyhow!(
637                        "logical permutation axis {index} is out of range for rank {}",
638                        self.axis_classes.len()
639                    ));
640                }
641                if seen[index] {
642                    return Err(anyhow::anyhow!("logical permutation repeats axis {index}"));
643                }
644                seen[index] = true;
645                Ok(self.axis_classes[index])
646            })
647            .collect::<Result<Vec<_>>>()
648            .map_err(StorageError::from)?;
649        Self::new(
650            self.data.clone(),
651            self.payload_dims.clone(),
652            self.strides.clone(),
653            axis_classes,
654        )
655        .map_err(StorageError::from)
656    }
657}
658
659impl<T: Copy> StructuredStorage<T> {
660    fn payload_scalar_at(&self, payload_coords: &[usize]) -> StorageResult<T> {
661        if payload_coords.len() != self.payload_dims.len() {
662            return Err(StorageError::InvalidStructuredStorage(format!(
663                "payload coordinate rank {} does not match payload rank {}",
664                payload_coords.len(),
665                self.payload_dims.len()
666            )));
667        }
668        let offset = payload_coords
669            .iter()
670            .zip(self.payload_dims.iter())
671            .zip(self.strides.iter())
672            .try_fold(0usize, |offset, ((&coordinate, &dim), &stride)| {
673                if coordinate >= dim {
674                    return Err(StorageError::InvalidStructuredStorage(format!(
675                        "payload coordinate {coordinate} is out of bounds for dim {dim}"
676                    )));
677                }
678                let stride = usize::try_from(stride).map_err(|_| {
679                    StorageError::InvalidStructuredStorage("negative stride".into())
680                })?;
681                let term = coordinate.checked_mul(stride).ok_or_else(|| {
682                    StorageError::InvalidStructuredStorage("payload offset overflow".into())
683                })?;
684                offset.checked_add(term).ok_or_else(|| {
685                    StorageError::InvalidStructuredStorage("payload offset overflow".into())
686                })
687            })?;
688        self.data.get(offset).copied().ok_or_else(|| {
689            StorageError::InvalidStructuredStorage("payload offset outside storage".into())
690        })
691    }
692
693    /// Maps payload elements while preserving payload metadata and axis classes.
694    ///
695    /// # Examples
696    ///
697    /// ```
698    /// use tensor4all_tensorbackend::StructuredStorage;
699    ///
700    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0, 3.0], &[3]).unwrap();
701    /// let doubled = s.map_copy(|x| x * 2.0);
702    /// assert_eq!(doubled.data(), &[2.0, 4.0, 6.0]);
703    /// ```
704    pub fn map_copy<U>(&self, mut f: impl FnMut(T) -> U) -> StructuredStorage<U> {
705        StructuredStorage {
706            data: self.data.iter().copied().map(&mut f).collect(),
707            payload_dims: self.payload_dims.clone(),
708            strides: self.strides.clone(),
709            axis_classes: self.axis_classes.clone(),
710        }
711    }
712}
713
714impl<T: Copy + Default> StructuredStorage<T> {
715    /// Returns the checked product of the logical dimensions, rejecting
716    /// overflow. The fallible dense exporters rely on this so that a logical
717    /// product that overflows `usize` fails closed instead of being treated
718    /// as an empty tensor.
719    fn checked_logical_len(&self) -> StorageResult<usize> {
720        let dims = self.logical_dims();
721        dims.iter()
722            .try_fold(1usize, |length, &dim| length.checked_mul(dim))
723            .ok_or_else(|| {
724                StorageError::InvalidStructuredStorage(format!(
725                    "logical dims product overflow for {dims:?}"
726                ))
727            })
728    }
729
730    /// Materializes the logical tensor as a contiguous column-major dense buffer.
731    ///
732    /// Repeated entries in `axis_classes` encode equality constraints between
733    /// logical axes. Logical indices that violate those constraints are
734    /// structural zeros in the dense materialization.
735    ///
736    /// # Errors
737    ///
738    /// Returns an error if the logical dimension product overflows `usize`.
739    ///
740    /// # Examples
741    ///
742    /// ```
743    /// use tensor4all_tensorbackend::StructuredStorage;
744    ///
745    /// // Diagonal [1, 2] in 2x2 becomes [1, 0, 0, 2] column-major
746    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
747    /// assert_eq!(d.logical_dense_col_major_vec().unwrap(), vec![1.0, 0.0, 0.0, 2.0]);
748    /// ```
749    pub fn logical_dense_col_major_vec(&self) -> StorageResult<Vec<T>> {
750        let logical_dims = self.logical_dims();
751        let logical_len = self.checked_logical_len()?;
752        if logical_len == 0 {
753            return Ok(Vec::new());
754        }
755        if let Some(view) = self.dense_col_major_view_if_contiguous() {
756            return Ok(view.to_vec());
757        }
758        if self.is_dense() {
759            return Ok(self.payload_col_major_vec());
760        }
761
762        let mut payload_index = vec![0usize; self.payload_dims.len()];
763        Ok((0..logical_len)
764            .map(|linear| {
765                let logical_index = col_major_multi_index(linear, &logical_dims);
766                self.value_at_with_dims(&logical_index, &logical_dims, &mut payload_index)
767                    .unwrap_or_default()
768            })
769            .collect())
770    }
771
772    fn for_each_payload_value(&self, mut f: impl FnMut(T)) {
773        if let Some(view) = self.payload_col_major_view_if_contiguous() {
774            for &value in view {
775                f(value);
776            }
777            return;
778        }
779        let payload_len = self
780            .payload_dims
781            .iter()
782            .try_fold(1usize, |length, &dim| length.checked_mul(dim))
783            .unwrap_or(0);
784        let mut payload_index = vec![0usize; self.payload_dims.len()];
785        for _ in 0..payload_len {
786            let Some(offset) = offset_from_strides(&payload_index, &self.strides) else {
787                return;
788            };
789            f(self.data[offset]);
790            let mut carry = true;
791            for (coordinate, &dim) in payload_index.iter_mut().zip(self.payload_dims.iter()) {
792                if !carry {
793                    break;
794                }
795                *coordinate += 1;
796                if *coordinate == dim {
797                    *coordinate = 0;
798                } else {
799                    carry = false;
800                }
801            }
802        }
803    }
804
805    fn value_at_with_dims(
806        &self,
807        logical_index: &[usize],
808        logical_dims: &[usize],
809        payload_index: &mut [usize],
810    ) -> StorageResult<T> {
811        if logical_index.len() != logical_dims.len() {
812            return Err(StorageError::InvalidStructuredStorage(format!(
813                "logical index rank {} does not match rank {}",
814                logical_index.len(),
815                logical_dims.len()
816            )));
817        }
818        if payload_index.len() < self.payload_dims.len() {
819            return Err(StorageError::InvalidStructuredStorage(format!(
820                "payload scratch rank {} is smaller than {}",
821                payload_index.len(),
822                self.payload_dims.len()
823            )));
824        }
825        payload_index[..self.payload_dims.len()].fill(usize::MAX);
826        for ((&value, &dim), &class_id) in logical_index
827            .iter()
828            .zip(logical_dims.iter())
829            .zip(self.axis_classes.iter())
830        {
831            if value >= dim {
832                return Err(StorageError::InvalidStructuredStorage(format!(
833                    "logical index {value} is out of bounds for dim {dim}"
834                )));
835            }
836            if payload_index[class_id] == usize::MAX {
837                payload_index[class_id] = value;
838            } else if payload_index[class_id] != value {
839                return Ok(T::default());
840            }
841        }
842        let offset = payload_index[..self.payload_dims.len()]
843            .iter()
844            .zip(self.strides.iter())
845            .try_fold(0usize, |offset, (&value, &stride)| {
846                let stride = usize::try_from(stride).map_err(|_| {
847                    StorageError::InvalidStructuredStorage("negative stride".into())
848                })?;
849                let term = value.checked_mul(stride).ok_or_else(|| {
850                    StorageError::InvalidStructuredStorage("payload offset overflow".into())
851                })?;
852                offset.checked_add(term).ok_or_else(|| {
853                    StorageError::InvalidStructuredStorage("payload offset overflow".into())
854                })
855            })?;
856        self.data.get(offset).copied().ok_or_else(|| {
857            StorageError::InvalidStructuredStorage("payload offset outside storage".into())
858        })
859    }
860}
861
862/// Storage backend for tensor data.
863///
864/// Public callers interact with this opaque wrapper through constructors and
865/// high-level query/materialization methods.
866///
867/// # Examples
868///
869/// ```
870/// use tensor4all_tensorbackend::Storage;
871///
872/// // Dense 2x3 matrix stored column-major: [[1,2,3],[4,5,6]]
873/// let data = vec![1.0_f64, 4.0, 2.0, 5.0, 3.0, 6.0];
874/// let s = Storage::from_dense_col_major(data, &[2, 3]).unwrap();
875/// assert!(s.is_f64());
876/// assert!(!s.is_complex());
877///
878/// // Diagonal storage: 2x2 identity-like diagonal
879/// let diag = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
880/// assert!(diag.is_f64());
881/// ```
882#[derive(Debug, Clone)]
883pub struct Storage(pub(crate) StorageRepr);
884
885/// Classifies the compact layout used by [`Storage`].
886///
887/// Use this to distinguish dense logical payloads from diagonal/copy payloads
888/// and general structured payloads without exposing the internal storage enum.
889///
890/// # Examples
891///
892/// ```
893/// use tensor4all_tensorbackend::{Storage, StorageKind};
894///
895/// let dense = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
896/// assert_eq!(dense.storage_kind(), StorageKind::Dense);
897///
898/// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
899/// assert_eq!(diag.storage_kind(), StorageKind::Diagonal);
900/// ```
901#[derive(Debug, Clone, Copy, PartialEq, Eq)]
902pub enum StorageKind {
903    /// Logical dense payload layout.
904    Dense,
905    /// Diagonal or copy-tensor payload layout.
906    Diagonal,
907    /// General structured payload layout with repeated axis classes.
908    Structured,
909}
910
911/// Errors returned by storage payload and elementwise operations.
912///
913/// Use this to distinguish scalar-kind mismatches, length mismatches, and
914/// invalid structured-storage metadata from general backend failures.
915///
916/// # Examples
917///
918/// ```
919/// use tensor4all_tensorbackend::{Storage, StorageError};
920///
921/// let storage = Storage::from_dense_col_major(vec![1.0_f64], &[1]).unwrap();
922/// let err = storage.payload_c64_col_major_vec().unwrap_err();
923/// assert!(matches!(err, StorageError::ScalarKindMismatch { .. }));
924/// ```
925#[derive(Debug, thiserror::Error)]
926pub enum StorageError {
927    /// The storage scalar kind did not match the requested operation.
928    #[error("expected {expected} storage when {operation}, got {actual}")]
929    ScalarKindMismatch {
930        /// The scalar kind that the caller requested.
931        expected: &'static str,
932        /// The scalar kind actually stored.
933        actual: &'static str,
934        /// Human-readable operation description.
935        operation: &'static str,
936    },
937    /// Two storages had different payload lengths for an elementwise operation.
938    #[error("storage lengths must match for {operation}: {left} != {right}")]
939    LengthMismatch {
940        /// Name of the operation being performed.
941        operation: &'static str,
942        /// Left-hand payload length.
943        left: usize,
944        /// Right-hand payload length.
945        right: usize,
946    },
947    /// Structured storage metadata was invalid after an operation.
948    #[error("invalid structured storage: {0}")]
949    InvalidStructuredStorage(String),
950    /// The requested operation does not support the provided storage kinds.
951    #[error("storage types are not supported for {operation}: {left} vs {right}")]
952    OperationNotSupported {
953        /// Name of the operation being performed.
954        operation: &'static str,
955        /// Left-hand storage kind.
956        left: &'static str,
957        /// Right-hand storage kind.
958        right: &'static str,
959    },
960    /// The requested operation requires real scalars but at least one scalar was complex.
961    #[error("expected real scalars in {operation} branch: a={a}, b={b}")]
962    RealScalarRequired {
963        /// Name of the operation being performed.
964        operation: &'static str,
965        /// Left scalar display string.
966        a: String,
967        /// Right scalar display string.
968        b: String,
969    },
970    /// A storage operation failed with an internal diagnostic.
971    #[error("storage operation failed: {source}")]
972    Operation {
973        /// Original internal diagnostic, preserving its source chain.
974        #[source]
975        source: anyhow::Error,
976    },
977}
978
979impl From<anyhow::Error> for StorageError {
980    fn from(source: anyhow::Error) -> Self {
981        Self::Operation { source }
982    }
983}
984
985/// Result type returned by storage methods that can fail with [`StorageError`].
986pub type StorageResult<T> = std::result::Result<T, StorageError>;
987
988#[derive(Debug, Clone)]
989pub(crate) enum StorageRepr {
990    /// Storage with f64 elements.
991    F64(StructuredStorage<f64>),
992    /// Storage with Complex64 elements.
993    C64(StructuredStorage<Complex64>),
994}
995
996fn storage_scalar_kind(repr: &StorageRepr) -> &'static str {
997    match repr {
998        StorageRepr::F64(_) => "f64",
999        StorageRepr::C64(_) => "Complex64",
1000    }
1001}
1002
1003/// Types that can be computed as the result of a reduction over `Storage`.
1004///
1005/// This lets callers write `let s: T = tensor.sum();` without matching on
1006/// the storage variant. Implemented for `f64` and `Complex64`.
1007///
1008/// # Examples
1009///
1010/// ```
1011/// use tensor4all_tensorbackend::{Storage, SumFromStorage};
1012///
1013/// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0], &[3]).unwrap();
1014/// let total: f64 = f64::sum_from_storage(&s);
1015/// assert!((total - 6.0).abs() < 1e-10);
1016/// ```
1017pub trait SumFromStorage: Sized {
1018    /// Compute the sum of all elements in the storage.
1019    fn sum_from_storage(storage: &Storage) -> Self;
1020}
1021
1022impl SumFromStorage for f64 {
1023    fn sum_from_storage(storage: &Storage) -> Self {
1024        let mut sum = 0.0;
1025        match &storage.0 {
1026            StorageRepr::F64(v) => v.for_each_payload_value(|value| sum += value),
1027            StorageRepr::C64(v) => v.for_each_payload_value(|value| sum += value.re),
1028        }
1029        sum
1030    }
1031}
1032
1033impl SumFromStorage for Complex64 {
1034    fn sum_from_storage(storage: &Storage) -> Self {
1035        let mut sum = Complex64::new(0.0, 0.0);
1036        match &storage.0 {
1037            StorageRepr::F64(v) => {
1038                v.for_each_payload_value(|value| sum += Complex64::new(value, 0.0))
1039            }
1040            StorageRepr::C64(v) => v.for_each_payload_value(|value| sum += value),
1041        }
1042        sum
1043    }
1044}
1045
1046use crate::any_scalar::BackendScalar;
1047
1048impl Storage {
1049    pub(crate) fn from_repr(repr: StorageRepr) -> Self {
1050        Self(repr)
1051    }
1052
1053    fn invalid_storage_error(err: anyhow::Error) -> StorageError {
1054        StorageError::from(err)
1055    }
1056
1057    #[cfg(test)]
1058    pub(crate) fn repr(&self) -> &StorageRepr {
1059        &self.0
1060    }
1061
1062    fn validate_dense_len<T>(data: &[T], logical_dims: &[usize], label: &str) -> Result<()> {
1063        let expected_len = logical_dims.iter().try_fold(1usize, |length, &dim| {
1064            length
1065                .checked_mul(dim)
1066                .ok_or_else(|| anyhow!("{label} dimension product overflows for {logical_dims:?}"))
1067        })?;
1068        ensure!(
1069            data.len() == expected_len,
1070            "{label} len {} does not match logical dims {:?} (expected {})",
1071            data.len(),
1072            logical_dims,
1073            expected_len
1074        );
1075        Ok(())
1076    }
1077
1078    /// Create dense storage from column-major logical values (generic over scalar type).
1079    ///
1080    /// The scalar type is inferred from the `data` argument.
1081    ///
1082    /// # Errors
1083    ///
1084    /// Returns an error when the data length does not match the logical dimension
1085    /// /// product (a shape mismatch).
1086    ///
1087    /// # Examples
1088    ///
1089    /// ```
1090    /// use tensor4all_tensorbackend::Storage;
1091    ///
1092    /// // 2x2 matrix, column-major: [[1,3],[2,4]]
1093    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0, 4.0], &[2, 2]).unwrap();
1094    /// assert!(s.is_f64());
1095    /// assert!(s.is_dense());
1096    /// assert_eq!(s.len(), 4);
1097    /// ```
1098    pub fn from_dense_col_major<T: StorageScalar>(
1099        data: Vec<T>,
1100        logical_dims: &[usize],
1101    ) -> Result<Self> {
1102        T::build_dense_storage(data, logical_dims)
1103    }
1104
1105    /// Create diagonal storage from column-major diagonal payload values (generic over scalar type).
1106    ///
1107    /// Creates a rank-2 diagonal storage by default. The scalar type is
1108    /// inferred from `diag_data`.
1109    ///
1110    /// # Errors
1111    ///
1112    /// Returns an error when the diagonal payload is incompatible with the logical
1113    /// /// rank (a shape mismatch).
1114    ///
1115    /// # Examples
1116    ///
1117    /// ```
1118    /// use tensor4all_tensorbackend::Storage;
1119    ///
1120    /// let s = Storage::from_diag_col_major(vec![1.0_f64, 2.0, 3.0], 2).unwrap();
1121    /// assert!(s.is_diag());
1122    /// assert!(s.is_f64());
1123    /// assert_eq!(s.len(), 3);
1124    /// ```
1125    pub fn from_diag_col_major<T: StorageScalar>(
1126        diag_data: Vec<T>,
1127        logical_rank: usize,
1128    ) -> Result<Self> {
1129        T::build_diag_storage(diag_data, logical_rank)
1130    }
1131
1132    /// Create a new 1D zero-initialized dense storage (generic over scalar type).
1133    ///
1134    /// # Examples
1135    ///
1136    /// ```
1137    /// use tensor4all_tensorbackend::Storage;
1138    ///
1139    /// let s = Storage::new_dense::<f64>(5).unwrap();
1140    /// assert!(s.is_dense());
1141    /// assert_eq!(s.len(), 5);
1142    /// assert!((s.max_abs()).abs() < 1e-10);
1143    /// ```
1144    ///
1145    /// # Errors
1146    ///
1147    /// Returns `StorageError` when the dimension is invalid (a shape
1148    /// mismatch).
1149    pub fn new_dense<T: StorageScalar + Default>(size: usize) -> StorageResult<Self> {
1150        Self::from_dense_col_major(vec![T::default(); size], &[size])
1151            .map_err(Self::invalid_storage_error)
1152    }
1153
1154    /// Create a new diagonal storage with the given diagonal data (generic over scalar type).
1155    ///
1156    /// # Errors
1157    ///
1158    /// Returns an error if diagonal metadata is invalid.
1159    ///
1160    /// # Examples
1161    ///
1162    /// ```
1163    /// use tensor4all_tensorbackend::Storage;
1164    ///
1165    /// let s = Storage::new_diag(vec![1.0_f64, 2.0, 3.0]).unwrap();
1166    /// assert!(s.is_diag());
1167    /// assert!(s.is_f64());
1168    /// ```
1169    pub fn new_diag<T: StorageScalar>(diag_data: Vec<T>) -> StorageResult<Self> {
1170        Self::from_diag_col_major(diag_data, 2).map_err(Self::invalid_storage_error)
1171    }
1172
1173    /// Create a new structured storage (generic over scalar type).
1174    ///
1175    /// # Errors
1176    ///
1177    /// Returns an error when the structured metadata is inconsistent (an
1178    /// /// invalid-storage failure).
1179    ///
1180    /// # Examples
1181    ///
1182    /// ```
1183    /// use tensor4all_tensorbackend::Storage;
1184    ///
1185    /// // Diagonal-like structured storage: axis_classes = [0, 0]
1186    /// let s = Storage::new_structured(
1187    ///     vec![1.0_f64, 2.0],
1188    ///     vec![2],         // payload_dims
1189    ///     vec![1],         // strides
1190    ///     vec![0, 0],      // axis_classes: both axes map to payload axis 0
1191    /// ).unwrap();
1192    /// assert!(s.is_diag());
1193    /// ```
1194    pub fn new_structured<T: StorageScalar>(
1195        data: Vec<T>,
1196        payload_dims: Vec<usize>,
1197        strides: Vec<isize>,
1198        axis_classes: Vec<usize>,
1199    ) -> Result<Self> {
1200        T::build_structured_storage(data, payload_dims, strides, axis_classes)
1201    }
1202
1203    /// Validate structured-storage metadata and return the required payload length.
1204    ///
1205    /// The metadata must be internally consistent: canonical axis classes, a
1206    /// payload rank implied by the axis classes, matching dim/stride ranks,
1207    /// non-negative strides, and size products that fit in `usize`. The
1208    /// returned length is exactly what a constructed [`Storage`] would
1209    /// require, so untrusted payload lengths can be rejected before any
1210    /// allocation.
1211    ///
1212    /// # Errors
1213    ///
1214    /// Returns an error if the metadata is inconsistent or its size products
1215    /// overflow `usize`.
1216    ///
1217    /// # Examples
1218    ///
1219    /// ```
1220    /// use tensor4all_tensorbackend::Storage;
1221    ///
1222    /// let required = Storage::validate_structured_metadata(
1223    ///     &[2],
1224    ///     &[1],
1225    ///     &[0, 0],
1226    /// ).unwrap();
1227    /// assert_eq!(required, 2);
1228    ///
1229    /// assert!(Storage::validate_structured_metadata(&[2], &[-1], &[0]).is_err());
1230    /// ```
1231    pub fn validate_structured_metadata(
1232        payload_dims: &[usize],
1233        strides: &[isize],
1234        axis_classes: &[usize],
1235    ) -> Result<usize> {
1236        validate_canonical_axis_classes(axis_classes)?;
1237        let payload_rank = axis_classes.iter().try_fold(0usize, |rank, &class_id| {
1238            let required_rank = class_id
1239                .checked_add(1)
1240                .ok_or_else(|| anyhow!("axis class index overflow"))?;
1241            Ok::<_, anyhow::Error>(rank.max(required_rank))
1242        })?;
1243        ensure!(
1244            payload_dims.len() == payload_rank,
1245            "payload rank {} does not match axis_classes {:?}",
1246            payload_dims.len(),
1247            axis_classes
1248        );
1249        ensure!(
1250            strides.len() == payload_dims.len(),
1251            "payload dims {:?} and strides {:?} must have the same rank",
1252            payload_dims,
1253            strides
1254        );
1255        required_storage_len(payload_dims, strides)
1256    }
1257
1258    /// Create dense f64 storage from column-major logical values.
1259    ///
1260    /// # Errors
1261    /// Create diagonal f64 storage from column-major diagonal payload values.
1262    ///
1263    /// # Errors
1264    ///
1265    /// Returns an error when the diagonal payload is incompatible with the logical
1266    /// /// rank (a shape mismatch).
1267    ///
1268    /// # Examples
1269    ///
1270    /// ```
1271    /// use tensor4all_tensorbackend::Storage;
1272    ///
1273    /// let s = Storage::from_diag_f64_col_major(vec![1.0, 2.0], 2).unwrap();
1274    /// assert!(s.is_diag());
1275    /// assert!(s.is_f64());
1276    /// ```
1277    pub fn from_diag_f64_col_major(
1278        diag_data: Vec<f64>,
1279        logical_rank: usize,
1280    ) -> StorageResult<Self> {
1281        Ok(Self::from_repr(StorageRepr::F64(
1282            StructuredStorage::from_diag_col_major(diag_data, logical_rank)?,
1283        )))
1284    }
1285
1286    /// Create diagonal Complex64 storage from column-major diagonal payload values.
1287    ///
1288    /// # Errors
1289    ///
1290    /// Returns an error when the diagonal payload is incompatible with the logical
1291    /// /// rank (a shape mismatch).
1292    ///
1293    /// # Examples
1294    ///
1295    /// ```
1296    /// use tensor4all_tensorbackend::Storage;
1297    /// use num_complex::Complex64;
1298    ///
1299    /// let data = vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)];
1300    /// let s = Storage::from_diag_c64_col_major(data, 2).unwrap();
1301    /// assert!(s.is_diag());
1302    /// assert!(s.is_c64());
1303    /// ```
1304    pub fn from_diag_c64_col_major(
1305        diag_data: Vec<Complex64>,
1306        logical_rank: usize,
1307    ) -> StorageResult<Self> {
1308        Ok(Self::from_repr(StorageRepr::C64(
1309            StructuredStorage::from_diag_col_major(diag_data, logical_rank)?,
1310        )))
1311    }
1312
1313    /// Check if this storage is logically dense.
1314    ///
1315    /// # Examples
1316    ///
1317    /// ```
1318    /// use tensor4all_tensorbackend::Storage;
1319    ///
1320    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
1321    /// assert!(s.is_dense());
1322    ///
1323    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
1324    /// assert!(!d.is_dense());
1325    /// ```
1326    pub fn is_dense(&self) -> bool {
1327        match &self.0 {
1328            StorageRepr::F64(value) => value.is_dense(),
1329            StorageRepr::C64(value) => value.is_dense(),
1330        }
1331    }
1332
1333    /// Check if this storage is a Diag storage type.
1334    ///
1335    /// # Examples
1336    ///
1337    /// ```
1338    /// use tensor4all_tensorbackend::Storage;
1339    ///
1340    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
1341    /// assert!(d.is_diag());
1342    /// ```
1343    pub fn is_diag(&self) -> bool {
1344        match &self.0 {
1345            StorageRepr::F64(value) => value.is_diag(),
1346            StorageRepr::C64(value) => value.is_diag(),
1347        }
1348    }
1349
1350    /// Returns the compact layout class for this storage.
1351    ///
1352    /// The return value is metadata-only and never materializes dense logical
1353    /// values. Use it to choose whether to read compact payload metadata or
1354    /// dense logical values.
1355    ///
1356    /// # Examples
1357    ///
1358    /// ```
1359    /// use tensor4all_tensorbackend::{Storage, StorageKind};
1360    ///
1361    /// let structured = Storage::new_structured(
1362    ///     vec![1.0_f64, 2.0],
1363    ///     vec![2],
1364    ///     vec![1],
1365    ///     vec![0, 0],
1366    /// ).unwrap();
1367    /// assert_eq!(structured.storage_kind(), StorageKind::Diagonal);
1368    /// ```
1369    pub fn storage_kind(&self) -> StorageKind {
1370        if self.is_dense() {
1371            StorageKind::Dense
1372        } else if self.is_diag() {
1373            StorageKind::Diagonal
1374        } else {
1375            StorageKind::Structured
1376        }
1377    }
1378
1379    /// Returns the logical tensor dimensions represented by this storage.
1380    ///
1381    /// The dimensions are derived from payload dimensions and `axis_classes`.
1382    ///
1383    /// # Examples
1384    ///
1385    /// ```
1386    /// use tensor4all_tensorbackend::Storage;
1387    ///
1388    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1389    /// assert_eq!(diag.logical_dims(), vec![2, 2]);
1390    /// ```
1391    pub fn logical_dims(&self) -> Vec<usize> {
1392        match &self.0 {
1393            StorageRepr::F64(value) => value.logical_dims(),
1394            StorageRepr::C64(value) => value.logical_dims(),
1395        }
1396    }
1397
1398    /// Returns the logical tensor rank represented by this storage.
1399    ///
1400    /// This equals `axis_classes().len()`, not necessarily `payload_dims().len()`.
1401    ///
1402    /// # Examples
1403    ///
1404    /// ```
1405    /// use tensor4all_tensorbackend::Storage;
1406    ///
1407    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 3).unwrap();
1408    /// assert_eq!(diag.logical_rank(), 3);
1409    /// assert_eq!(diag.payload_dims(), &[2]);
1410    /// ```
1411    pub fn logical_rank(&self) -> usize {
1412        match &self.0 {
1413            StorageRepr::F64(value) => value.logical_rank(),
1414            StorageRepr::C64(value) => value.logical_rank(),
1415        }
1416    }
1417
1418    /// Returns the compact payload dimensions.
1419    ///
1420    /// For dense storage these match logical dimensions. For diagonal storage
1421    /// this is rank-1 even when the logical tensor has multiple axes.
1422    ///
1423    /// # Examples
1424    ///
1425    /// ```
1426    /// use tensor4all_tensorbackend::Storage;
1427    ///
1428    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1429    /// assert_eq!(diag.payload_dims(), &[2]);
1430    /// ```
1431    pub fn payload_dims(&self) -> &[usize] {
1432        match &self.0 {
1433            StorageRepr::F64(value) => value.payload_dims(),
1434            StorageRepr::C64(value) => value.payload_dims(),
1435        }
1436    }
1437
1438    /// Returns the compact payload strides.
1439    ///
1440    /// Strides are measured in stored scalar elements and describe the compact
1441    /// payload buffer, not the logical dense tensor.
1442    ///
1443    /// # Examples
1444    ///
1445    /// ```
1446    /// use tensor4all_tensorbackend::Storage;
1447    ///
1448    /// let dense = Storage::from_dense_col_major(vec![0.0_f64; 6], &[2, 3]).unwrap();
1449    /// assert_eq!(dense.payload_strides(), &[1, 2]);
1450    /// ```
1451    pub fn payload_strides(&self) -> &[isize] {
1452        match &self.0 {
1453            StorageRepr::F64(value) => value.strides(),
1454            StorageRepr::C64(value) => value.strides(),
1455        }
1456    }
1457
1458    /// Returns logical-axis equivalence classes for this storage.
1459    ///
1460    /// Repeated class labels mean the corresponding logical axes share one
1461    /// payload axis. Dense storage has `[0, 1, ...]`; diagonal storage has
1462    /// repeated zero labels.
1463    ///
1464    /// # Examples
1465    ///
1466    /// ```
1467    /// use tensor4all_tensorbackend::Storage;
1468    ///
1469    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1470    /// assert_eq!(diag.axis_classes(), &[0, 0]);
1471    /// ```
1472    pub fn axis_classes(&self) -> &[usize] {
1473        match &self.0 {
1474            StorageRepr::F64(value) => value.axis_classes(),
1475            StorageRepr::C64(value) => value.axis_classes(),
1476        }
1477    }
1478
1479    /// Returns the number of stored compact payload elements.
1480    ///
1481    /// For dense storage this equals the logical dense length. For diagonal and
1482    /// structured storage this is the compact payload length.
1483    ///
1484    /// # Examples
1485    ///
1486    /// ```
1487    /// use tensor4all_tensorbackend::Storage;
1488    ///
1489    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1490    /// assert_eq!(diag.payload_len(), 2);
1491    /// ```
1492    pub fn payload_len(&self) -> usize {
1493        self.len()
1494    }
1495
1496    /// Copies the compact `f64` payload in column-major payload order.
1497    ///
1498    /// This does not materialize logical dense values. For diagonal storage the
1499    /// returned vector contains only diagonal payload values.
1500    ///
1501    /// # Errors
1502    ///
1503    /// Returns an error if the storage scalar type is not `f64`.
1504    ///
1505    /// # Examples
1506    ///
1507    /// ```
1508    /// use tensor4all_tensorbackend::Storage;
1509    ///
1510    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1511    /// assert_eq!(diag.payload_f64_col_major_vec().unwrap(), vec![1.0, 2.0]);
1512    /// ```
1513    pub fn payload_f64_col_major_vec(&self) -> StorageResult<Vec<f64>> {
1514        match &self.0 {
1515            StorageRepr::F64(value) => Ok(value.payload_col_major_vec()),
1516            StorageRepr::C64(_) => Err(StorageError::ScalarKindMismatch {
1517                expected: "f64",
1518                actual: storage_scalar_kind(&self.0),
1519                operation: "copying f64 payload",
1520            }),
1521        }
1522    }
1523
1524    /// Borrows the compact `f64` payload when it is already contiguous in
1525    /// column-major payload order.
1526    ///
1527    /// # Errors
1528    ///
1529    /// Returns `StorageError::ScalarKindMismatch` when the storage is not
1530    /// f64-backed (a dtype mismatch).
1531    pub fn payload_f64_col_major_view_if_contiguous(&self) -> StorageResult<Option<&[f64]>> {
1532        match &self.0 {
1533            StorageRepr::F64(value) => Ok(value.payload_col_major_view_if_contiguous()),
1534            StorageRepr::C64(_) => Err(StorageError::ScalarKindMismatch {
1535                expected: "f64",
1536                actual: storage_scalar_kind(&self.0),
1537                operation: "borrowing f64 payload",
1538            }),
1539        }
1540    }
1541
1542    /// Copies the compact `Complex64` payload in column-major payload order.
1543    ///
1544    /// This does not materialize logical dense values. Complex payloads are
1545    /// returned as native Rust `Complex64` values.
1546    ///
1547    /// # Errors
1548    ///
1549    /// Returns an error if the storage scalar type is not `Complex64`.
1550    ///
1551    /// # Examples
1552    ///
1553    /// ```
1554    /// use num_complex::Complex64;
1555    /// use tensor4all_tensorbackend::Storage;
1556    ///
1557    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)];
1558    /// let diag = Storage::from_diag_col_major(data.clone(), 2).unwrap();
1559    /// assert_eq!(diag.payload_c64_col_major_vec().unwrap(), data);
1560    /// ```
1561    pub fn payload_c64_col_major_vec(&self) -> StorageResult<Vec<Complex64>> {
1562        match &self.0 {
1563            StorageRepr::C64(value) => Ok(value.payload_col_major_vec()),
1564            StorageRepr::F64(_) => Err(StorageError::ScalarKindMismatch {
1565                expected: "Complex64",
1566                actual: storage_scalar_kind(&self.0),
1567                operation: "copying c64 payload",
1568            }),
1569        }
1570    }
1571
1572    /// Borrows the compact `Complex64` payload when it is already contiguous in
1573    /// column-major payload order.
1574    ///
1575    /// # Errors
1576    ///
1577    /// Returns `StorageError::ScalarKindMismatch` when the storage is not
1578    /// c64-backed (a dtype mismatch).
1579    pub fn payload_c64_col_major_view_if_contiguous(&self) -> StorageResult<Option<&[Complex64]>> {
1580        match &self.0 {
1581            StorageRepr::C64(value) => Ok(value.payload_col_major_view_if_contiguous()),
1582            StorageRepr::F64(_) => Err(StorageError::ScalarKindMismatch {
1583                expected: "Complex64",
1584                actual: storage_scalar_kind(&self.0),
1585                operation: "borrowing c64 payload",
1586            }),
1587        }
1588    }
1589
1590    /// Check if this storage uses f64 scalar type.
1591    ///
1592    /// # Examples
1593    ///
1594    /// ```
1595    /// use tensor4all_tensorbackend::Storage;
1596    ///
1597    /// let s = Storage::from_dense_col_major(vec![1.0_f64], &[1]).unwrap();
1598    /// assert!(s.is_f64());
1599    /// assert!(!s.is_c64());
1600    /// ```
1601    pub fn is_f64(&self) -> bool {
1602        matches!(&self.0, StorageRepr::F64(_))
1603    }
1604
1605    /// Check if this storage uses Complex64 scalar type.
1606    ///
1607    /// # Examples
1608    ///
1609    /// ```
1610    /// use tensor4all_tensorbackend::Storage;
1611    /// use num_complex::Complex64;
1612    ///
1613    /// let s = Storage::from_dense_col_major(
1614    ///     vec![Complex64::new(1.0, 0.0)], &[1],
1615    /// ).unwrap();
1616    /// assert!(s.is_c64());
1617    /// ```
1618    pub fn is_c64(&self) -> bool {
1619        matches!(&self.0, StorageRepr::C64(_))
1620    }
1621
1622    /// Check if this storage uses complex scalar type.
1623    ///
1624    /// This is an alias for [`is_c64()`](Self::is_c64).
1625    ///
1626    /// # Examples
1627    ///
1628    /// ```
1629    /// use tensor4all_tensorbackend::Storage;
1630    /// use num_complex::Complex64;
1631    ///
1632    /// let s = Storage::from_dense_col_major(
1633    ///     vec![Complex64::new(1.0, 0.0)], &[1],
1634    /// ).unwrap();
1635    /// assert!(s.is_complex());
1636    ///
1637    /// let r = Storage::from_dense_col_major(vec![1.0_f64], &[1]).unwrap();
1638    /// assert!(!r.is_complex());
1639    /// ```
1640    pub fn is_complex(&self) -> bool {
1641        self.is_c64()
1642    }
1643
1644    /// Get the length of the storage payload (number of stored elements).
1645    ///
1646    /// For dense storage this equals the product of logical dimensions.
1647    /// For diagonal storage this equals the diagonal length.
1648    ///
1649    /// # Examples
1650    ///
1651    /// ```
1652    /// use tensor4all_tensorbackend::Storage;
1653    ///
1654    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0], &[3]).unwrap();
1655    /// assert_eq!(s.len(), 3);
1656    ///
1657    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
1658    /// assert_eq!(d.len(), 2);
1659    /// ```
1660    pub fn len(&self) -> usize {
1661        match &self.0 {
1662            StorageRepr::F64(v) => v.len(),
1663            StorageRepr::C64(v) => v.len(),
1664        }
1665    }
1666
1667    /// Check if the storage is empty.
1668    ///
1669    /// # Examples
1670    ///
1671    /// ```
1672    /// use tensor4all_tensorbackend::Storage;
1673    ///
1674    /// let s = Storage::new_dense::<f64>(0).unwrap();
1675    /// assert!(s.is_empty());
1676    ///
1677    /// let s2 = Storage::new_dense::<f64>(3).unwrap();
1678    /// assert!(!s2.is_empty());
1679    /// ```
1680    pub fn is_empty(&self) -> bool {
1681        self.len() == 0
1682    }
1683
1684    /// Sum all referenced compact payload entries, converting to type `T`.
1685    ///
1686    /// Stride gaps and padding in a structured backing buffer are not tensor
1687    /// values and are excluded from the reduction.
1688    ///
1689    /// # Example
1690    /// ```
1691    /// use tensor4all_tensorbackend::Storage;
1692    /// let s = Storage::from_dense_col_major(vec![1.0, 2.0, 3.0], &[3]).unwrap();
1693    /// assert_eq!(s.sum::<f64>(), 6.0);
1694    /// ```
1695    pub fn sum<T: SumFromStorage>(&self) -> T {
1696        T::sum_from_storage(self)
1697    }
1698
1699    /// Maximum absolute value over all referenced compact payload entries.
1700    ///
1701    /// Stride gaps and padding are excluded. For real storage this is `max(|x|)`, and for complex storage this is
1702    /// `max(hypot(re, im))`. NaN payloads, including a NaN real or imaginary
1703    /// component paired with infinity, propagate to a NaN result; positive
1704    /// infinity is preserved.
1705    ///
1706    /// # Examples
1707    ///
1708    /// ```
1709    /// use tensor4all_tensorbackend::Storage;
1710    ///
1711    /// let s = Storage::from_dense_col_major(vec![-3.0_f64, 1.0, 2.0], &[3]).unwrap();
1712    /// assert!((s.max_abs() - 3.0).abs() < 1e-10);
1713    /// ```
1714    pub fn max_abs(&self) -> f64 {
1715        fn fold_nan_propagating(values: impl IntoIterator<Item = f64>) -> f64 {
1716            values.into_iter().fold(0.0_f64, |current, value| {
1717                if current.is_nan() || value.is_nan() {
1718                    f64::NAN
1719                } else {
1720                    current.max(value)
1721                }
1722            })
1723        }
1724
1725        let mut current = 0.0_f64;
1726        let mut update = |value: f64| {
1727            current = fold_nan_propagating([current, value]);
1728        };
1729        match &self.0 {
1730            StorageRepr::F64(v) => v.for_each_payload_value(|value| update(value.abs())),
1731            StorageRepr::C64(v) => v.for_each_payload_value(|z| {
1732                update(if z.re.is_nan() || z.im.is_nan() {
1733                    f64::NAN
1734                } else {
1735                    z.re.hypot(z.im)
1736                });
1737            }),
1738        }
1739        current
1740    }
1741
1742    /// Scan the compact payload without copying it.
1743    ///
1744    /// The returned flags describe the stored payload, not the logical dense
1745    /// tensor. Structural zeros cannot introduce non-finite values, so this is
1746    /// sufficient for metric validation while preserving compact memory use.
1747    ///
1748    /// # Examples
1749    ///
1750    /// ```
1751    /// use tensor4all_tensorbackend::Storage;
1752    ///
1753    /// let storage = Storage::from_dense_col_major(vec![1.0_f64, f64::INFINITY], &[2]).unwrap();
1754    /// assert_eq!(storage.payload_nonfinite_flags(), (false, true));
1755    /// ```
1756    pub fn payload_nonfinite_flags(&self) -> (bool, bool) {
1757        match &self.0 {
1758            StorageRepr::F64(value) => {
1759                let mut flags = (false, false);
1760                value.for_each_payload_value(|value| {
1761                    flags.0 |= value.is_nan();
1762                    flags.1 |= value.is_infinite();
1763                });
1764                flags
1765            }
1766            StorageRepr::C64(value) => {
1767                let mut flags = (false, false);
1768                value.for_each_payload_value(|value| {
1769                    flags.0 |= value.re.is_nan() || value.im.is_nan();
1770                    flags.1 |= value.re.is_infinite() || value.im.is_infinite();
1771                });
1772                flags
1773            }
1774        }
1775    }
1776
1777    /// Return one compact payload value as a dynamic [`BackendScalar`].
1778    ///
1779    /// `payload_coords` are column-major payload coordinates, not logical
1780    /// coordinates. Repeated logical axis classes are represented once in the
1781    /// payload, so this lookup performs only rank-bounded stride arithmetic and
1782    /// never allocates coordinates or scans the payload.
1783    ///
1784    /// # Errors
1785    ///
1786    /// Returns [`StorageError`] when the coordinate rank or bounds are invalid
1787    /// or the compact metadata points outside the backing payload.
1788    ///
1789    /// # Examples
1790    ///
1791    /// ```
1792    /// use num_complex::Complex64;
1793    /// use tensor4all_tensorbackend::Storage;
1794    ///
1795    /// let real = Storage::from_diag_col_major(vec![2.0_f64, 3.0], 2).unwrap();
1796    /// assert_eq!(real.scalar_at(&[1]).unwrap().real(), 3.0);
1797    ///
1798    /// let complex = Storage::from_dense_col_major(
1799    ///     vec![Complex64::new(1.0, -2.0)], &[1],
1800    /// ).unwrap();
1801    /// assert_eq!(complex.scalar_at(&[0]).unwrap().as_c64(),
1802    ///     Some(Complex64::new(1.0, -2.0)));
1803    /// ```
1804    pub fn scalar_at(&self, payload_coords: &[usize]) -> StorageResult<BackendScalar> {
1805        match &self.0 {
1806            StorageRepr::F64(value) => value
1807                .payload_scalar_at(payload_coords)
1808                .map(BackendScalar::from_value),
1809            StorageRepr::C64(value) => value
1810                .payload_scalar_at(payload_coords)
1811                .map(BackendScalar::from_value),
1812        }
1813    }
1814
1815    /// Materialize dense logical values as a column-major `f64` buffer.
1816    ///
1817    /// For diagonal storage, off-diagonal entries are filled with zero.
1818    ///
1819    /// # Errors
1820    ///
1821    /// Returns an error if the storage is complex or `logical_dims` does not
1822    /// match the stored logical dimensions.
1823    ///
1824    /// # Examples
1825    ///
1826    /// ```
1827    /// use tensor4all_tensorbackend::Storage;
1828    ///
1829    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0, 4.0], &[2, 2]).unwrap();
1830    /// let dense = s.to_dense_f64_col_major_vec(&[2, 2]).unwrap();
1831    /// assert_eq!(dense, vec![1.0, 2.0, 3.0, 4.0]);
1832    /// ```
1833    pub fn to_dense_f64_col_major_vec(&self, logical_dims: &[usize]) -> StorageResult<Vec<f64>> {
1834        match &self.0 {
1835            StorageRepr::F64(v) => {
1836                let structured_dims = v.logical_dims();
1837                if structured_dims != logical_dims {
1838                    return Err(StorageError::InvalidStructuredStorage(format!(
1839                        "logical dims {:?} do not match StructuredF64 logical dims {:?}",
1840                        logical_dims, structured_dims
1841                    )));
1842                }
1843                v.logical_dense_col_major_vec()
1844            }
1845            StorageRepr::C64(_) => Err(StorageError::ScalarKindMismatch {
1846                expected: "f64",
1847                actual: storage_scalar_kind(&self.0),
1848                operation: "materializing dense f64 values",
1849            }),
1850        }
1851    }
1852
1853    /// Materialize dense logical values as a column-major `Complex64` buffer.
1854    ///
1855    /// # Errors
1856    ///
1857    /// Returns an error if the storage is real or `logical_dims` does not
1858    /// match the stored logical dimensions.
1859    ///
1860    /// # Examples
1861    ///
1862    /// ```
1863    /// use tensor4all_tensorbackend::Storage;
1864    /// use num_complex::Complex64;
1865    ///
1866    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)];
1867    /// let s = Storage::from_dense_col_major(data.clone(), &[2]).unwrap();
1868    /// let dense = s.to_dense_c64_col_major_vec(&[2]).unwrap();
1869    /// assert_eq!(dense, data);
1870    /// ```
1871    pub fn to_dense_c64_col_major_vec(
1872        &self,
1873        logical_dims: &[usize],
1874    ) -> StorageResult<Vec<Complex64>> {
1875        match &self.0 {
1876            StorageRepr::C64(v) => {
1877                let structured_dims = v.logical_dims();
1878                if structured_dims != logical_dims {
1879                    return Err(StorageError::InvalidStructuredStorage(format!(
1880                        "logical dims {:?} do not match StructuredC64 logical dims {:?}",
1881                        logical_dims, structured_dims
1882                    )));
1883                }
1884                v.logical_dense_col_major_vec()
1885            }
1886            StorageRepr::F64(_) => Err(StorageError::ScalarKindMismatch {
1887                expected: "Complex64",
1888                actual: storage_scalar_kind(&self.0),
1889                operation: "materializing dense c64 values",
1890            }),
1891        }
1892    }
1893
1894    /// Convert this storage to dense storage.
1895    ///
1896    /// For Diag storage, creates a Dense storage with diagonal elements set
1897    /// and off-diagonal elements as zero. For Dense storage, returns a copy.
1898    ///
1899    /// # Errors
1900    ///
1901    /// Returns an error if `dims` does not match the stored logical dimensions
1902    /// or if dense storage construction fails.
1903    ///
1904    /// # Examples
1905    ///
1906    /// ```
1907    /// use tensor4all_tensorbackend::Storage;
1908    ///
1909    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
1910    /// let dense = d.to_dense_storage(&[2, 2]).unwrap();
1911    /// assert!(dense.is_dense());
1912    /// let vals = dense.to_dense_f64_col_major_vec(&[2, 2]).unwrap();
1913    /// assert_eq!(vals, vec![1.0, 0.0, 0.0, 2.0]);
1914    /// ```
1915    pub fn to_dense_storage(&self, dims: &[usize]) -> StorageResult<Storage> {
1916        if self.is_f64() {
1917            let values = self.to_dense_f64_col_major_vec(dims)?;
1918            Storage::from_dense_col_major(values, dims).map_err(Self::invalid_storage_error)
1919        } else {
1920            let values = self.to_dense_c64_col_major_vec(dims)?;
1921            Storage::from_dense_col_major(values, dims).map_err(Self::invalid_storage_error)
1922        }
1923    }
1924
1925    /// Permute the storage data according to the given permutation.
1926    ///
1927    /// The `_dims` parameter is currently unused (reserved for future use).
1928    ///
1929    /// # Examples
1930    ///
1931    /// ```
1932    /// use tensor4all_tensorbackend::Storage;
1933    ///
1934    /// // Diagonal 2x2 tensor, permute axes (identity perm for diag is valid)
1935    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
1936    /// let t = d.permute_storage(&[2, 2], &[1, 0]).unwrap();
1937    /// assert!(t.is_diag());
1938    /// ```
1939    ///
1940    /// # Errors
1941    ///
1942    /// Returns `StorageError` when the permutation is invalid (an
1943    /// invalid-index failure).
1944    pub fn permute_storage(&self, _dims: &[usize], perm: &[usize]) -> StorageResult<Storage> {
1945        match &self.0 {
1946            StorageRepr::F64(v) => Ok(Storage::from_repr(StorageRepr::F64(
1947                v.permute_logical_axes(perm)?,
1948            ))),
1949            StorageRepr::C64(v) => Ok(Storage::from_repr(StorageRepr::C64(
1950                v.permute_logical_axes(perm)?,
1951            ))),
1952        }
1953    }
1954
1955    /// Extract real part from Complex64 storage as f64 storage.
1956    /// For f64 storage, returns a copy (clone).
1957    ///
1958    /// # Examples
1959    ///
1960    /// ```
1961    /// use tensor4all_tensorbackend::Storage;
1962    /// use num_complex::Complex64;
1963    ///
1964    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)];
1965    /// let s = Storage::from_dense_col_major(data, &[2]).unwrap();
1966    /// let re = s.extract_real_part();
1967    /// assert!(re.is_f64());
1968    /// assert_eq!(re.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![1.0, 3.0]);
1969    /// ```
1970    pub fn extract_real_part(&self) -> Storage {
1971        match &self.0 {
1972            StorageRepr::F64(v) => Storage::from_repr(StorageRepr::F64(v.clone())),
1973            StorageRepr::C64(v) => Storage::from_repr(StorageRepr::F64(v.map_copy(|z| z.re))),
1974        }
1975    }
1976
1977    /// Extract imaginary part from Complex64 storage as f64 storage.
1978    /// For f64 storage, returns zero storage.
1979    ///
1980    /// # Examples
1981    ///
1982    /// ```
1983    /// use tensor4all_tensorbackend::Storage;
1984    /// use num_complex::Complex64;
1985    ///
1986    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)];
1987    /// let s = Storage::from_dense_col_major(data, &[2]).unwrap();
1988    /// let im = s.extract_imag_part(&[2]);
1989    /// assert!(im.is_f64());
1990    /// assert_eq!(im.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![2.0, 4.0]);
1991    /// ```
1992    pub fn extract_imag_part(&self, _dims: &[usize]) -> Storage {
1993        match &self.0 {
1994            StorageRepr::F64(v) => Storage::from_repr(StorageRepr::F64(v.map_copy(|_| 0.0))),
1995            StorageRepr::C64(v) => Storage::from_repr(StorageRepr::F64(v.map_copy(|z| z.im))),
1996        }
1997    }
1998
1999    /// Convert f64 storage to Complex64 storage (real part only, imaginary part is zero).
2000    /// For Complex64 storage, returns a clone.
2001    ///
2002    /// # Examples
2003    ///
2004    /// ```
2005    /// use tensor4all_tensorbackend::Storage;
2006    ///
2007    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
2008    /// let c = s.to_complex_storage();
2009    /// assert!(c.is_c64());
2010    /// ```
2011    pub fn to_complex_storage(&self) -> Storage {
2012        match &self.0 {
2013            StorageRepr::F64(v) => {
2014                Storage::from_repr(StorageRepr::C64(v.map_copy(|x| Complex64::new(x, 0.0))))
2015            }
2016            StorageRepr::C64(v) => Storage::from_repr(StorageRepr::C64(v.clone())),
2017        }
2018    }
2019
2020    /// Complex conjugate of all elements.
2021    ///
2022    /// For real (f64) storage, returns a clone (conjugate of real is identity).
2023    /// For complex (Complex64) storage, conjugates each element.
2024    ///
2025    /// This is inspired by the `conj` operation in ITensorMPS.jl.
2026    ///
2027    /// # Examples
2028    /// ```
2029    /// use tensor4all_tensorbackend::Storage;
2030    /// use num_complex::Complex64;
2031    ///
2032    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, -4.0)];
2033    /// let storage = Storage::from_dense_col_major(data, &[2]).unwrap();
2034    /// let conj_storage = storage.conj();
2035    ///
2036    /// let result = conj_storage.to_dense_c64_col_major_vec(&[2]).unwrap();
2037    /// assert_eq!(result[0], Complex64::new(1.0, -2.0));
2038    /// assert_eq!(result[1], Complex64::new(3.0, 4.0));
2039    /// ```
2040    pub fn conj(&self) -> Self {
2041        match &self.0 {
2042            StorageRepr::F64(v) => Storage::from_repr(StorageRepr::F64(v.clone())),
2043            StorageRepr::C64(v) => Storage::from_repr(StorageRepr::C64(v.map_copy(|z| z.conj()))),
2044        }
2045    }
2046
2047    /// Combine two f64 storages into Complex64 storage.
2048    ///
2049    /// `real_storage` becomes the real part, `imag_storage` becomes the imaginary part.
2050    /// Formula: `real + i * imag`.
2051    ///
2052    /// # Errors
2053    ///
2054    /// Returns an error if either storage is not `f64`, if their payload lengths
2055    /// differ, or if the result metadata is invalid.
2056    ///
2057    /// # Examples
2058    ///
2059    /// ```
2060    /// use tensor4all_tensorbackend::Storage;
2061    /// use num_complex::Complex64;
2062    ///
2063    /// let re = Storage::from_dense_col_major(vec![1.0_f64, 3.0], &[2]).unwrap();
2064    /// let im = Storage::from_dense_col_major(vec![2.0_f64, 4.0], &[2]).unwrap();
2065    /// let c = Storage::combine_to_complex(&re, &im).unwrap();
2066    /// assert!(c.is_c64());
2067    /// let vals = c.to_dense_c64_col_major_vec(&[2]).unwrap();
2068    /// assert_eq!(vals[0], Complex64::new(1.0, 2.0));
2069    /// assert_eq!(vals[1], Complex64::new(3.0, 4.0));
2070    /// ```
2071    pub fn combine_to_complex(
2072        real_storage: &Storage,
2073        imag_storage: &Storage,
2074    ) -> StorageResult<Storage> {
2075        match (&real_storage.0, &imag_storage.0) {
2076            (StorageRepr::F64(real), StorageRepr::F64(imag)) => {
2077                if real.len() != imag.len() {
2078                    return Err(StorageError::LengthMismatch {
2079                        operation: "combine_to_complex",
2080                        left: real.len(),
2081                        right: imag.len(),
2082                    });
2083                }
2084                let complex_vec: Vec<Complex64> = real
2085                    .data()
2086                    .iter()
2087                    .zip(imag.data().iter())
2088                    .map(|(&r, &i)| Complex64::new(r, i))
2089                    .collect();
2090                Ok(Storage::from_repr(StorageRepr::C64(
2091                    StructuredStorage::new(
2092                        complex_vec,
2093                        real.payload_dims().to_vec(),
2094                        real.strides().to_vec(),
2095                        real.axis_classes().to_vec(),
2096                    )
2097                    .map_err(Self::invalid_storage_error)?,
2098                )))
2099            }
2100            _ => Err(StorageError::OperationNotSupported {
2101                operation: "combine_to_complex",
2102                left: storage_scalar_kind(&real_storage.0),
2103                right: storage_scalar_kind(&imag_storage.0),
2104            }),
2105        }
2106    }
2107
2108    /// Add two storages element-wise, returning `Result` on error instead of panicking.
2109    ///
2110    /// Both storages must have the same type and length.
2111    ///
2112    /// # Errors
2113    ///
2114    /// Returns an error if storage types or lengths don't match.
2115    ///
2116    /// # Examples
2117    ///
2118    /// ```
2119    /// use tensor4all_tensorbackend::Storage;
2120    ///
2121    /// let a = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
2122    /// let b = Storage::from_dense_col_major(vec![3.0_f64, 4.0], &[2]).unwrap();
2123    /// let c = a.try_add(&b).unwrap();
2124    /// assert_eq!(c.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![4.0, 6.0]);
2125    /// ```
2126    pub fn try_add(&self, other: &Storage) -> StorageResult<Storage> {
2127        match (&self.0, &other.0) {
2128            (StorageRepr::F64(a), StorageRepr::F64(b)) => {
2129                if a.len() != b.len() {
2130                    return Err(StorageError::LengthMismatch {
2131                        operation: "addition",
2132                        left: a.len(),
2133                        right: b.len(),
2134                    });
2135                }
2136                let sum_vec: Vec<f64> = a
2137                    .data()
2138                    .iter()
2139                    .zip(b.data().iter())
2140                    .map(|(&x, &y)| x + y)
2141                    .collect();
2142                Ok(Storage::from_repr(StorageRepr::F64(
2143                    StructuredStorage::new(
2144                        sum_vec,
2145                        a.payload_dims().to_vec(),
2146                        a.strides().to_vec(),
2147                        a.axis_classes().to_vec(),
2148                    )
2149                    .map_err(|err| StorageError::InvalidStructuredStorage(err.to_string()))?,
2150                )))
2151            }
2152            (StorageRepr::C64(a), StorageRepr::C64(b)) => {
2153                if a.len() != b.len() {
2154                    return Err(StorageError::LengthMismatch {
2155                        operation: "addition",
2156                        left: a.len(),
2157                        right: b.len(),
2158                    });
2159                }
2160                let sum_vec: Vec<Complex64> = a
2161                    .data()
2162                    .iter()
2163                    .zip(b.data().iter())
2164                    .map(|(&x, &y)| x + y)
2165                    .collect();
2166                Ok(Storage::from_repr(StorageRepr::C64(
2167                    StructuredStorage::new(
2168                        sum_vec,
2169                        a.payload_dims().to_vec(),
2170                        a.strides().to_vec(),
2171                        a.axis_classes().to_vec(),
2172                    )
2173                    .map_err(|err| StorageError::InvalidStructuredStorage(err.to_string()))?,
2174                )))
2175            }
2176            _ => Err(StorageError::OperationNotSupported {
2177                operation: "addition",
2178                left: storage_scalar_kind(&self.0),
2179                right: storage_scalar_kind(&other.0),
2180            }),
2181        }
2182    }
2183
2184    /// Try to subtract two storages element-wise.
2185    ///
2186    /// # Errors
2187    ///
2188    /// Returns an error if the storages have different types or lengths.
2189    ///
2190    /// # Examples
2191    ///
2192    /// ```
2193    /// use tensor4all_tensorbackend::Storage;
2194    ///
2195    /// let a = Storage::from_dense_col_major(vec![5.0_f64, 7.0], &[2]).unwrap();
2196    /// let b = Storage::from_dense_col_major(vec![1.0_f64, 3.0], &[2]).unwrap();
2197    /// let c = a.try_sub(&b).unwrap();
2198    /// assert_eq!(c.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![4.0, 4.0]);
2199    /// ```
2200    pub fn try_sub(&self, other: &Storage) -> StorageResult<Storage> {
2201        match (&self.0, &other.0) {
2202            (StorageRepr::F64(a), StorageRepr::F64(b)) => {
2203                if a.len() != b.len() {
2204                    return Err(StorageError::LengthMismatch {
2205                        operation: "subtraction",
2206                        left: a.len(),
2207                        right: b.len(),
2208                    });
2209                }
2210                let diff_vec: Vec<f64> = a
2211                    .data()
2212                    .iter()
2213                    .zip(b.data().iter())
2214                    .map(|(&x, &y)| x - y)
2215                    .collect();
2216                Ok(Storage::from_repr(StorageRepr::F64(
2217                    StructuredStorage::new(
2218                        diff_vec,
2219                        a.payload_dims().to_vec(),
2220                        a.strides().to_vec(),
2221                        a.axis_classes().to_vec(),
2222                    )
2223                    .map_err(|err| StorageError::InvalidStructuredStorage(err.to_string()))?,
2224                )))
2225            }
2226            (StorageRepr::C64(a), StorageRepr::C64(b)) => {
2227                if a.len() != b.len() {
2228                    return Err(StorageError::LengthMismatch {
2229                        operation: "subtraction",
2230                        left: a.len(),
2231                        right: b.len(),
2232                    });
2233                }
2234                let diff_vec: Vec<Complex64> = a
2235                    .data()
2236                    .iter()
2237                    .zip(b.data().iter())
2238                    .map(|(&x, &y)| x - y)
2239                    .collect();
2240                Ok(Storage::from_repr(StorageRepr::C64(
2241                    StructuredStorage::new(
2242                        diff_vec,
2243                        a.payload_dims().to_vec(),
2244                        a.strides().to_vec(),
2245                        a.axis_classes().to_vec(),
2246                    )
2247                    .map_err(|err| StorageError::InvalidStructuredStorage(err.to_string()))?,
2248                )))
2249            }
2250            _ => Err(StorageError::OperationNotSupported {
2251                operation: "subtraction",
2252                left: storage_scalar_kind(&self.0),
2253                right: storage_scalar_kind(&other.0),
2254            }),
2255        }
2256    }
2257
2258    /// Scale storage by a scalar value.
2259    ///
2260    /// If the scalar is complex but the storage is real, the storage is promoted to complex.
2261    ///
2262    /// # Examples
2263    ///
2264    /// ```
2265    /// use tensor4all_tensorbackend::{BackendScalar, Storage};
2266    ///
2267    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0], &[3]).unwrap();
2268    /// let scaled = s.scale(&BackendScalar::new_real(2.0));
2269    /// assert_eq!(scaled.to_dense_f64_col_major_vec(&[3]).unwrap(), vec![2.0, 4.0, 6.0]);
2270    /// ```
2271    pub fn scale(&self, scalar: &crate::BackendScalar) -> Storage {
2272        self * scalar.clone()
2273    }
2274
2275    /// Compute linear combination: `a * self + b * other`.
2276    ///
2277    /// # Errors
2278    ///
2279    /// Returns an error if the storages have different types or lengths.
2280    /// If any scalar is complex, the result is promoted to complex.
2281    ///
2282    /// # Examples
2283    ///
2284    /// ```
2285    /// use tensor4all_tensorbackend::{BackendScalar, Storage};
2286    ///
2287    /// let x = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
2288    /// let y = Storage::from_dense_col_major(vec![3.0_f64, 4.0], &[2]).unwrap();
2289    /// let a = BackendScalar::new_real(2.0);
2290    /// let b = BackendScalar::new_real(3.0);
2291    /// // result = 2*[1,2] + 3*[3,4] = [11, 16]
2292    /// let result = x.axpby(&a, &y, &b).unwrap();
2293    /// assert_eq!(result.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![11.0, 16.0]);
2294    /// ```
2295    pub fn axpby(
2296        &self,
2297        a: &crate::BackendScalar,
2298        other: &Storage,
2299        b: &crate::BackendScalar,
2300    ) -> StorageResult<Storage> {
2301        // First check lengths match
2302        if self.len() != other.len() {
2303            return Err(StorageError::LengthMismatch {
2304                operation: "axpby",
2305                left: self.len(),
2306                right: other.len(),
2307            });
2308        }
2309
2310        // Determine if we need complex output
2311        let needs_complex = a.is_complex()
2312            || b.is_complex()
2313            || matches!(&self.0, StorageRepr::C64(_))
2314            || matches!(&other.0, StorageRepr::C64(_));
2315
2316        if needs_complex {
2317            // Promote everything to complex
2318            let a_c: Complex64 = a.clone().into();
2319            let b_c: Complex64 = b.clone().into();
2320
2321            let (result, payload_dims, strides, axis_classes): (
2322                Vec<Complex64>,
2323                Vec<usize>,
2324                Vec<isize>,
2325                Vec<usize>,
2326            ) = match (&self.0, &other.0) {
2327                (StorageRepr::F64(x), StorageRepr::F64(y)) => (
2328                    x.data()
2329                        .iter()
2330                        .zip(y.data().iter())
2331                        .map(|(&xi, &yi)| {
2332                            a_c * Complex64::new(xi, 0.0) + b_c * Complex64::new(yi, 0.0)
2333                        })
2334                        .collect(),
2335                    x.payload_dims().to_vec(),
2336                    x.strides().to_vec(),
2337                    x.axis_classes().to_vec(),
2338                ),
2339                (StorageRepr::F64(x), StorageRepr::C64(y)) => (
2340                    x.data()
2341                        .iter()
2342                        .zip(y.data().iter())
2343                        .map(|(&xi, &yi)| a_c * Complex64::new(xi, 0.0) + b_c * yi)
2344                        .collect(),
2345                    x.payload_dims().to_vec(),
2346                    x.strides().to_vec(),
2347                    x.axis_classes().to_vec(),
2348                ),
2349                (StorageRepr::C64(x), StorageRepr::F64(y)) => (
2350                    x.data()
2351                        .iter()
2352                        .zip(y.data().iter())
2353                        .map(|(&xi, &yi)| a_c * xi + b_c * Complex64::new(yi, 0.0))
2354                        .collect(),
2355                    x.payload_dims().to_vec(),
2356                    x.strides().to_vec(),
2357                    x.axis_classes().to_vec(),
2358                ),
2359                (StorageRepr::C64(x), StorageRepr::C64(y)) => (
2360                    x.data()
2361                        .iter()
2362                        .zip(y.data().iter())
2363                        .map(|(&xi, &yi)| a_c * xi + b_c * yi)
2364                        .collect(),
2365                    x.payload_dims().to_vec(),
2366                    x.strides().to_vec(),
2367                    x.axis_classes().to_vec(),
2368                ),
2369            };
2370            Ok(Storage::from_repr(StorageRepr::C64(
2371                StructuredStorage::new(result, payload_dims, strides, axis_classes)
2372                    .map_err(|err| StorageError::InvalidStructuredStorage(err.to_string()))?,
2373            )))
2374        } else {
2375            // All real
2376            if !a.is_real() || !b.is_real() {
2377                return Err(StorageError::RealScalarRequired {
2378                    operation: "real axpby",
2379                    a: a.to_string(),
2380                    b: b.to_string(),
2381                });
2382            }
2383            let a_f = a.real();
2384            let b_f = b.real();
2385
2386            match (&self.0, &other.0) {
2387                (StorageRepr::F64(x), StorageRepr::F64(y)) => {
2388                    let result: Vec<f64> = x
2389                        .data()
2390                        .iter()
2391                        .zip(y.data().iter())
2392                        .map(|(&xi, &yi)| a_f * xi + b_f * yi)
2393                        .collect();
2394                    Ok(Storage::from_repr(StorageRepr::F64(
2395                        StructuredStorage::new(
2396                            result,
2397                            x.payload_dims().to_vec(),
2398                            x.strides().to_vec(),
2399                            x.axis_classes().to_vec(),
2400                        )
2401                        .map_err(|err| StorageError::InvalidStructuredStorage(err.to_string()))?,
2402                    )))
2403                }
2404                _ => Err(StorageError::OperationNotSupported {
2405                    operation: "axpby",
2406                    left: storage_scalar_kind(&self.0),
2407                    right: storage_scalar_kind(&other.0),
2408                }),
2409            }
2410        }
2411    }
2412}
2413
2414/// Helper to get a mutable reference to storage, cloning if needed (COW).
2415///
2416/// Uses `Arc::make_mut` semantics: if the `Arc` has only one strong reference,
2417/// returns a mutable reference to the existing allocation. Otherwise clones
2418/// the inner value first.
2419///
2420/// # Examples
2421///
2422/// ```
2423/// use std::sync::Arc;
2424/// use tensor4all_tensorbackend::{make_mut_storage, Storage};
2425///
2426/// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
2427/// let mut arc = Arc::new(s);
2428/// let s_mut = make_mut_storage(&mut arc);
2429/// // s_mut is now a mutable reference to Storage
2430/// assert!(s_mut.is_f64());
2431/// ```
2432pub fn make_mut_storage(arc: &mut Arc<Storage>) -> &mut Storage {
2433    Arc::make_mut(arc)
2434}
2435
2436/// Get the minimum dimension from a slice of dimensions.
2437///
2438/// Returns 1 for an empty slice. This is used for DiagTensor where all
2439/// indices must have the same dimension.
2440///
2441/// # Examples
2442///
2443/// ```
2444/// use tensor4all_tensorbackend::min_dim;
2445///
2446/// assert_eq!(min_dim(&[2, 3, 4]), 2);
2447/// assert_eq!(min_dim(&[5, 5, 5]), 5);
2448/// assert_eq!(min_dim(&[]), 1);
2449/// ```
2450pub fn min_dim(dims: &[usize]) -> usize {
2451    dims.iter().copied().min().unwrap_or(1)
2452}
2453
2454/// Contract two storage tensors along specified axes.
2455///
2456/// All storage is StructuredStorage; contraction is delegated to the native
2457/// tenferro backend. This is the primary tensor contraction entry point at
2458/// the storage layer.
2459///
2460/// # Arguments
2461///
2462/// * `storage_a` - First tensor storage
2463/// * `dims_a` - Dimensions of the first tensor
2464/// * `axes_a` - Axes of the first tensor to contract
2465/// * `storage_b` - Second tensor storage
2466/// * `dims_b` - Dimensions of the second tensor
2467/// * `axes_b` - Axes of the second tensor to contract
2468/// * `result_dims` - Dimensions of the result tensor (empty for scalar result)
2469///
2470/// # Returns
2471/// A new `Storage` containing the contracted result.
2472///
2473/// # Errors
2474///
2475/// Returns an error if axes are invalid, contracted dimensions do not match, or
2476/// the native backend rejects the contraction.
2477///
2478/// # Examples
2479///
2480/// ```
2481/// use tensor4all_tensorbackend::{contract_storage, Storage};
2482///
2483/// // Matrix-vector multiply: A(2x3) * v(3) -> result(2)
2484/// let a = Storage::from_dense_col_major(
2485///     vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3],
2486/// ).unwrap();
2487/// let v = Storage::from_dense_col_major(vec![1.0, 1.0, 1.0], &[3]).unwrap();
2488/// let result = contract_storage(&a, &[2, 3], &[1], &v, &[3], &[0], &[2]).unwrap();
2489/// // Row sums: [1+3+5, 2+4+6] = [9, 12]
2490/// let vals = result.to_dense_f64_col_major_vec(&[2]).unwrap();
2491/// assert!((vals[0] - 9.0).abs() < 1e-10);
2492/// assert!((vals[1] - 12.0).abs() < 1e-10);
2493/// ```
2494pub fn contract_storage(
2495    storage_a: &Storage,
2496    dims_a: &[usize],
2497    axes_a: &[usize],
2498    storage_b: &Storage,
2499    dims_b: &[usize],
2500    axes_b: &[usize],
2501    result_dims: &[usize],
2502) -> StorageResult<Storage> {
2503    try_contract_storage(
2504        storage_a,
2505        dims_a,
2506        axes_a,
2507        storage_b,
2508        dims_b,
2509        axes_b,
2510        result_dims,
2511    )
2512}
2513
2514fn try_contract_storage(
2515    storage_a: &Storage,
2516    dims_a: &[usize],
2517    axes_a: &[usize],
2518    storage_b: &Storage,
2519    dims_b: &[usize],
2520    axes_b: &[usize],
2521    result_dims: &[usize],
2522) -> StorageResult<Storage> {
2523    if axes_a.len() != axes_b.len() {
2524        return Err(StorageError::InvalidStructuredStorage(format!(
2525            "contract axes lengths must match: {} != {}",
2526            axes_a.len(),
2527            axes_b.len()
2528        )));
2529    }
2530
2531    for (&a_axis, &b_axis) in axes_a.iter().zip(axes_b.iter()) {
2532        let Some(&a_dim) = dims_a.get(a_axis) else {
2533            return Err(StorageError::InvalidStructuredStorage(format!(
2534                "contract axis {a_axis} is out of range for left dims {dims_a:?}"
2535            )));
2536        };
2537        let Some(&b_dim) = dims_b.get(b_axis) else {
2538            return Err(StorageError::InvalidStructuredStorage(format!(
2539                "contract axis {b_axis} is out of range for right dims {dims_b:?}"
2540            )));
2541        };
2542        if a_dim != b_dim {
2543            return Err(StorageError::InvalidStructuredStorage(format!(
2544                "contracted dimensions must match: dims_a[{a_axis}] = {a_dim} != dims_b[{b_axis}] = {b_dim}"
2545            )));
2546        }
2547    }
2548
2549    crate::tenferro_bridge::contract_storage_native(
2550        storage_a,
2551        dims_a,
2552        axes_a,
2553        storage_b,
2554        dims_b,
2555        axes_b,
2556        result_dims,
2557    )
2558    .map_err(|err| StorageError::InvalidStructuredStorage(err.to_string()))
2559}
2560
2561/// Multiply storage by a scalar (f64).
2562/// For Complex64 storage, multiplies each element by the scalar (treated as real).
2563impl Mul<f64> for &Storage {
2564    type Output = Storage;
2565
2566    fn mul(self, scalar: f64) -> Self::Output {
2567        match &self.0 {
2568            StorageRepr::F64(v) => Storage::from_repr(StorageRepr::F64(v.map_copy(|x| x * scalar))),
2569            StorageRepr::C64(v) => Storage::from_repr(StorageRepr::C64(
2570                v.map_copy(|z| z * Complex64::new(scalar, 0.0)),
2571            )),
2572        }
2573    }
2574}
2575
2576/// Multiply storage by a scalar (Complex64).
2577impl Mul<Complex64> for &Storage {
2578    type Output = Storage;
2579
2580    fn mul(self, scalar: Complex64) -> Self::Output {
2581        match &self.0 {
2582            StorageRepr::F64(v) => Storage::from_repr(StorageRepr::C64(
2583                v.map_copy(|x| Complex64::new(x, 0.0) * scalar),
2584            )),
2585            StorageRepr::C64(v) => Storage::from_repr(StorageRepr::C64(v.map_copy(|z| z * scalar))),
2586        }
2587    }
2588}
2589
2590/// Multiply storage by a scalar (BackendScalar).
2591/// May promote f64 storage to Complex64 when scalar is complex.
2592impl Mul<BackendScalar> for &Storage {
2593    type Output = Storage;
2594
2595    fn mul(self, scalar: BackendScalar) -> Self::Output {
2596        if scalar.is_complex() {
2597            let z: Complex64 = scalar.into();
2598            self * z
2599        } else {
2600            self * scalar.real()
2601        }
2602    }
2603}
2604
2605#[cfg(test)]
2606mod tests;