Skip to main content

tensor4all_tensorbackend/
storage.rs

1use crate::any_scalar::BackendScalar;
2use crate::tensor_element::TensorElement;
3use anyhow::{anyhow, ensure, Result};
4use num_complex::{Complex32, Complex64};
5use std::ops::Mul;
6use std::sync::Arc;
7use tenferro::{DType, TensorScalar};
8
9/// Dispatch a compact-payload body once over every scalar type [`Storage`] holds.
10///
11/// The four erased arms live here and nowhere else; `$ty` names the payload
12/// scalar type inside `$body`, so dtype-agnostic storage methods are written
13/// exactly once. Do not name `$ty` after an enclosing generic parameter: the
14/// expansion introduces a local type alias with that name.
15macro_rules! storage_payload_dispatch {
16    ($repr:expr, $ty:ident => |$payload:ident| $body:expr) => {
17        match $repr {
18            StorageRepr::F32($payload) => {
19                #[allow(unused)] // the body names the payload type only when it needs it
20                type $ty = f32;
21                $body
22            }
23            StorageRepr::F64($payload) => {
24                #[allow(unused)] // the body names the payload type only when it needs it
25                type $ty = f64;
26                $body
27            }
28            StorageRepr::C32($payload) => {
29                #[allow(unused)] // the body names the payload type only when it needs it
30                type $ty = Complex32;
31                $body
32            }
33            StorageRepr::C64($payload) => {
34                #[allow(unused)] // the body names the payload type only when it needs it
35                type $ty = Complex64;
36                $body
37            }
38        }
39    };
40}
41
42/// Dispatch a two-operand body over payloads that share one scalar type.
43///
44/// Elementwise storage algebra is only defined for matching scalar kinds, so
45/// the four same-dtype arms are written once and every other combination falls
46/// through to `else <expr>`.
47macro_rules! storage_payload_same_dtype_dispatch {
48    ($left:expr, $right:expr, $ty:ident => |$a:ident, $b:ident| $body:expr, else $fallback:expr) => {
49        match ($left, $right) {
50            (StorageRepr::F32($a), StorageRepr::F32($b)) => {
51                #[allow(unused)] // the body names the payload type only when it needs it
52                type $ty = f32;
53                $body
54            }
55            (StorageRepr::F64($a), StorageRepr::F64($b)) => {
56                #[allow(unused)] // the body names the payload type only when it needs it
57                type $ty = f64;
58                $body
59            }
60            (StorageRepr::C32($a), StorageRepr::C32($b)) => {
61                #[allow(unused)] // the body names the payload type only when it needs it
62                type $ty = Complex32;
63                $body
64            }
65            (StorageRepr::C64($a), StorageRepr::C64($b)) => {
66                #[allow(unused)] // the body names the payload type only when it needs it
67                type $ty = Complex64;
68                $body
69            }
70            _ => $fallback,
71        }
72    };
73}
74
75/// Dispatch a two-payload body over every scalar-type pair.
76///
77/// The nesting is intentional: each payload keeps its own concrete scalar type,
78/// so cross-dtype algebra (mixed promotion) is written once instead of in
79/// sixteen hand-written arms.
80macro_rules! storage_payload_pair_dispatch {
81    ($left:expr, $right:expr, $left_ty:ident, $right_ty:ident => |$a:ident, $b:ident| $body:expr) => {
82        storage_payload_dispatch!($left, $left_ty => |$a| {
83            storage_payload_dispatch!($right, $right_ty => |$b| $body)
84        })
85    };
86}
87
88/// Dispatch a body over a computed result dtype, binding `$ty` to it.
89///
90/// Used where promotion decides the result scalar type before the payload can
91/// be built, so the arithmetic is written once and narrowed or widened by
92/// [`StorageScalar::from_widened_parts`].
93macro_rules! storage_dtype_dispatch {
94    ($dtype:expr, $ty:ident => $body:expr, else $fallback:expr) => {
95        match $dtype {
96            DType::F32 => {
97                #[allow(unused)] // the body names the payload type only when it needs it
98                type $ty = f32;
99                $body
100            }
101            DType::F64 => {
102                #[allow(unused)] // the body names the payload type only when it needs it
103                type $ty = f64;
104                $body
105            }
106            DType::C32 => {
107                #[allow(unused)] // the body names the payload type only when it needs it
108                type $ty = Complex32;
109                $body
110            }
111            DType::C64 => {
112                #[allow(unused)] // the body names the payload type only when it needs it
113                type $ty = Complex64;
114                $body
115            }
116            _ => $fallback,
117        }
118    };
119}
120
121pub(crate) use storage_dtype_dispatch;
122
123/// Named result of a non-finite scan over stored scalar values.
124///
125/// The two flags are independent: a payload can contain both NaN and infinity,
126/// and `Default` is the all-finite result. Use it instead of a positional
127/// `(has_nan, has_infinity)` pair so call sites name the flag they read.
128///
129/// This reports what the scan found; it does not classify, replace, or mask the
130/// values. See [`Storage::payload_nonfinite_flags`] for the compact-payload
131/// scan and `tensor4all_core` for the native-tensor scan over a different
132/// representation.
133///
134/// # Examples
135///
136/// ```
137/// use tensor4all_tensorbackend::NonfiniteFlags;
138///
139/// let finite = NonfiniteFlags::default();
140/// assert!(!finite.has_nan && !finite.has_infinity);
141///
142/// let flags = NonfiniteFlags {
143///     has_nan: true,
144///     has_infinity: false,
145/// };
146/// assert!(flags.has_nan);
147/// assert_ne!(flags, finite);
148/// ```
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
150pub struct NonfiniteFlags {
151    /// At least one scanned real or imaginary component is NaN.
152    pub has_nan: bool,
153    /// At least one scanned real or imaginary component is infinite.
154    pub has_infinity: bool,
155}
156
157/// Trait for scalar types that can be stored in [`Storage`].
158///
159/// This enables generic constructors such as [`Storage::from_dense_col_major`]
160/// and [`Storage::from_diag_col_major`], and it is the single seam through
161/// which dtype-erased storage code reaches a concrete payload. Implemented for
162/// `f64`, `Complex64`, `f32`, and `Complex32`.
163///
164/// Compact storage never widens a payload: `f32` stays `f32` and `Complex32`
165/// stays `Complex32` in memory and on every accessor. The associated items
166/// describe the real/complex structure so that dtype-agnostic scans can be
167/// written once; explicit width promotion happens only in the storage algebra
168/// that documents it ([`Storage::axpby`], [`Storage::scale`]).
169///
170/// # Examples
171///
172/// ```
173/// use tensor4all_tensorbackend::{Storage, StorageScalar};
174///
175/// // Using the generic constructor -- scalar type is inferred from data
176/// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0], &[3]).unwrap();
177/// assert!(s.is_f64());
178///
179/// use num_complex::Complex64;
180/// let c = Storage::from_dense_col_major(
181///     vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)],
182///     &[2],
183/// ).unwrap();
184/// assert!(c.is_c64());
185///
186/// // Narrow payloads keep their dtype instead of widening.
187/// let narrow = Storage::from_dense_col_major(vec![1.0_f32, 2.0], &[2]).unwrap();
188/// assert!(narrow.is_f32());
189/// assert_eq!(f32::KIND, "f32");
190/// assert_eq!(Complex64::KIND, "Complex64");
191/// ```
192pub trait StorageScalar: TensorElement + Default + PartialEq + num_traits::Num {
193    /// Display name of this scalar type in diagnostics (`"f32"`, `"Complex64"`, ...).
194    const KIND: &'static str;
195    /// Whether values carry an imaginary component.
196    const IS_COMPLEX: bool;
197    /// The complex counterpart of this scalar type (`f32` -> `Complex32`).
198    type Complex: StorageScalar;
199
200    /// Widen this value to `(real, imaginary)` parts for dtype-agnostic scans.
201    fn widened_parts(self) -> (f64, f64);
202
203    /// Narrow widened parts back to this scalar type.
204    fn from_widened_parts(re: f64, im: f64) -> Self;
205
206    /// Real part as this scalar's real counterpart.
207    fn real_part(self) -> Self::Real;
208
209    /// Imaginary part as this scalar's real counterpart (`0` for real scalars).
210    fn imag_part(self) -> Self::Real;
211
212    /// Embed this value, including its imaginary part, in [`Self::Complex`].
213    fn into_complex(self) -> Self::Complex;
214
215    /// The complex conjugate (`self` for real scalars).
216    fn conjugate(self) -> Self;
217
218    /// Wrap a compact payload of this scalar type as a [`Storage`].
219    fn wrap_payload(payload: StructuredStorage<Self>) -> Storage;
220
221    /// Copy this value into a dynamic [`BackendScalar`].
222    fn to_backend_scalar(self) -> BackendScalar;
223
224    /// Copy the compact payload of this scalar type, rejecting other dtypes.
225    /// # Errors
226    ///
227    /// Returns [`StorageError::ScalarKindMismatch`] when `storage` holds a
228    /// different scalar type.
229    fn payload_vec(storage: &Storage) -> StorageResult<Vec<Self>>;
230
231    /// Borrow the compact payload of this scalar type when it is contiguous.
232    /// # Errors
233    ///
234    /// Returns [`StorageError::ScalarKindMismatch`] when `storage` holds a
235    /// different scalar type.
236    fn payload_view(storage: &Storage) -> StorageResult<Option<&[Self]>>;
237
238    /// Materialize the logical dense values of this scalar type.
239    /// # Errors
240    ///
241    /// Returns [`StorageError::ScalarKindMismatch`] when `storage` holds a
242    /// different scalar type, and
243    /// [`StorageError::InvalidStructuredStorage`] when `logical_dims` does not
244    /// match the stored logical dimensions or the logical element count
245    /// overflows `usize`.
246    fn dense_values(storage: &Storage, logical_dims: &[usize]) -> StorageResult<Vec<Self>>;
247
248    /// Build a dense [`Storage`] from column-major data.
249    /// # Errors
250    ///
251    /// Returns an error when the shape product would overflow `usize` or
252    /// `data.len()` does not match the expected shape.
253    ///
254    fn build_dense_storage(data: Vec<Self>, logical_dims: &[usize]) -> Result<Storage> {
255        let label = format!("dense {} payload", Self::KIND);
256        Storage::validate_dense_len(&data, logical_dims, &label)?;
257        Ok(Self::wrap_payload(StructuredStorage::from_dense_col_major(
258            data,
259            logical_dims,
260        )?))
261    }
262    /// Build a diagonal [`Storage`] from diagonal payload data.
263    /// # Errors
264    ///
265    /// Returns an error when the diagonal payload length does not match the
266    /// length required for `logical_rank` or a stride product would overflow
267    /// `usize`.
268    ///
269    fn build_diag_storage(diag_data: Vec<Self>, logical_rank: usize) -> Result<Storage> {
270        Ok(Self::wrap_payload(StructuredStorage::from_diag_col_major(
271            diag_data,
272            logical_rank,
273        )?))
274    }
275    /// Build a structured [`Storage`] from explicit payload metadata.
276    /// # Errors
277    ///
278    /// Returns an error when `axis_classes` is not in canonical
279    /// first-appearance order, when the `payload_dims` or `strides` ranks do
280    /// not match `axis_classes`, when a stride is negative, when a size
281    /// product would overflow `usize`, or when `data.len()` does not match the
282    /// required payload length.
283    ///
284    fn build_structured_storage(
285        data: Vec<Self>,
286        payload_dims: Vec<usize>,
287        strides: Vec<isize>,
288        axis_classes: Vec<usize>,
289    ) -> Result<Storage> {
290        Ok(Self::wrap_payload(StructuredStorage::new(
291            data,
292            payload_dims,
293            strides,
294            axis_classes,
295        )?))
296    }
297}
298
299/// Implement [`StorageScalar`] for one concrete scalar type.
300///
301/// Only the per-dtype algebra (real/imaginary extraction, complex embedding,
302/// conjugation, widening) is spelled per type; payload construction and
303/// dispatch are shared.
304macro_rules! impl_storage_scalar {
305    ($ty:ty, $variant:ident, $complex:ty, $kind:literal, $is_complex:literal,
306     copy_operation: $copy_operation:literal, view_operation: $view_operation:literal,
307     dense_operation: $dense_operation:literal,
308     widened: $widened:expr, from_widened: $from_widened:expr,
309     real: $real:expr, imag: $imag:expr, into_complex: $into_complex:expr,
310     conj: $conj:expr) => {
311        impl StorageScalar for $ty {
312            const KIND: &'static str = $kind;
313            const IS_COMPLEX: bool = $is_complex;
314            type Complex = $complex;
315
316            fn widened_parts(self) -> (f64, f64) {
317                ($widened)(self)
318            }
319
320            fn from_widened_parts(re: f64, im: f64) -> Self {
321                ($from_widened)(re, im)
322            }
323
324            fn real_part(self) -> Self::Real {
325                ($real)(self)
326            }
327
328            fn imag_part(self) -> Self::Real {
329                ($imag)(self)
330            }
331
332            fn into_complex(self) -> Self::Complex {
333                ($into_complex)(self)
334            }
335
336            fn conjugate(self) -> Self {
337                ($conj)(self)
338            }
339
340            fn wrap_payload(payload: StructuredStorage<Self>) -> Storage {
341                Storage(StorageRepr::$variant(payload))
342            }
343
344            fn to_backend_scalar(self) -> BackendScalar {
345                BackendScalar::from_value(self)
346            }
347
348            fn payload_vec(storage: &Storage) -> StorageResult<Vec<Self>> {
349                match &storage.0 {
350                    StorageRepr::$variant(payload) => Ok(payload.payload_col_major_vec()),
351                    _ => Err(StorageError::ScalarKindMismatch {
352                        expected: Self::KIND,
353                        actual: storage.scalar_kind(),
354                        operation: $copy_operation,
355                    }),
356                }
357            }
358
359            fn payload_view(storage: &Storage) -> StorageResult<Option<&[Self]>> {
360                match &storage.0 {
361                    StorageRepr::$variant(payload) => {
362                        Ok(payload.payload_col_major_view_if_contiguous())
363                    }
364                    _ => Err(StorageError::ScalarKindMismatch {
365                        expected: Self::KIND,
366                        actual: storage.scalar_kind(),
367                        operation: $view_operation,
368                    }),
369                }
370            }
371
372            fn dense_values(storage: &Storage, logical_dims: &[usize]) -> StorageResult<Vec<Self>> {
373                match &storage.0 {
374                    StorageRepr::$variant(payload) => checked_dense_payload(payload, logical_dims),
375                    _ => Err(StorageError::ScalarKindMismatch {
376                        expected: Self::KIND,
377                        actual: storage.scalar_kind(),
378                        operation: $dense_operation,
379                    }),
380                }
381            }
382        }
383    };
384}
385
386impl_storage_scalar!(
387    f32,
388    F32,
389    Complex32,
390    "f32",
391    false,
392    copy_operation: "copying f32 payload",
393    view_operation: "borrowing f32 payload",
394    dense_operation: "materializing dense f32 values",
395    widened: |value: f32| (f64::from(value), 0.0),
396    from_widened: |re: f64, _im: f64| re as f32,
397    real: |value: f32| value,
398    imag: |_value: f32| 0.0,
399    into_complex: |value: f32| Complex32::new(value, 0.0),
400    conj: |value: f32| value
401);
402
403impl_storage_scalar!(
404    f64,
405    F64,
406    Complex64,
407    "f64",
408    false,
409    copy_operation: "copying f64 payload",
410    view_operation: "borrowing f64 payload",
411    dense_operation: "materializing dense f64 values",
412    widened: |value: f64| (value, 0.0),
413    from_widened: |re: f64, _im: f64| re,
414    real: |value: f64| value,
415    imag: |_value: f64| 0.0,
416    into_complex: |value: f64| Complex64::new(value, 0.0),
417    conj: |value: f64| value
418);
419
420impl_storage_scalar!(
421    Complex32,
422    C32,
423    Complex32,
424    "Complex32",
425    true,
426    copy_operation: "copying c32 payload",
427    view_operation: "borrowing c32 payload",
428    dense_operation: "materializing dense c32 values",
429    widened: |value: Complex32| (f64::from(value.re), f64::from(value.im)),
430    from_widened: |re: f64, im: f64| Complex32::new(re as f32, im as f32),
431    real: |value: Complex32| value.re,
432    imag: |value: Complex32| value.im,
433    into_complex: |value: Complex32| value,
434    conj: |value: Complex32| value.conj()
435);
436
437impl_storage_scalar!(
438    Complex64,
439    C64,
440    Complex64,
441    "Complex64",
442    true,
443    copy_operation: "copying c64 payload",
444    view_operation: "borrowing c64 payload",
445    dense_operation: "materializing dense c64 values",
446    widened: |value: Complex64| (value.re, value.im),
447    from_widened: |re: f64, im: f64| Complex64::new(re, im),
448    real: |value: Complex64| value.re,
449    imag: |value: Complex64| value.im,
450    into_complex: |value: Complex64| value,
451    conj: |value: Complex64| value.conj()
452);
453
454pub(crate) fn col_major_strides(dims: &[usize]) -> Result<Vec<isize>> {
455    let mut strides = Vec::with_capacity(dims.len());
456    let mut stride = 1isize;
457    for &dim in dims {
458        strides.push(stride);
459        let dim = isize::try_from(dim)
460            .map_err(|_| anyhow!("column-major stride overflow for dims {dims:?}"))?;
461        stride = stride
462            .checked_mul(dim)
463            .ok_or_else(|| anyhow!("column-major stride overflow for dims {dims:?}"))?;
464    }
465    Ok(strides)
466}
467
468fn validate_canonical_axis_classes(axis_classes: &[usize]) -> Result<()> {
469    let mut next_class = 0usize;
470    for &class_id in axis_classes {
471        ensure!(
472            class_id <= next_class,
473            "axis_classes must be canonical first-appearance labels, got {axis_classes:?}"
474        );
475        if class_id == next_class {
476            next_class = next_class
477                .checked_add(1)
478                .ok_or_else(|| anyhow!("axis class index overflow"))?;
479        }
480    }
481    Ok(())
482}
483
484fn required_storage_len(dims: &[usize], strides: &[isize]) -> Result<usize> {
485    ensure!(
486        dims.len() == strides.len(),
487        "payload dims {:?} and strides {:?} must have the same rank",
488        dims,
489        strides
490    );
491
492    // Validate the compact payload product even when a zero stride would make
493    // the referenced backing span look small. Iteration must never begin with
494    // a wrapped payload length.
495    let payload_len = dims.iter().try_fold(1usize, |length, &dim| {
496        length
497            .checked_mul(dim)
498            .ok_or_else(|| anyhow!("payload length overflow for dims {dims:?}"))
499    })?;
500
501    let mut max_offset = 0usize;
502    for (&dim, &stride) in dims.iter().zip(strides.iter()) {
503        ensure!(
504            stride >= 0,
505            "negative strides are not supported in StructuredStorage: {strides:?}"
506        );
507        let stride = usize::try_from(stride)
508            .map_err(|_| anyhow!("payload stride overflow for dims {dims:?}"))?;
509        if dim > 1 {
510            let span = (dim - 1)
511                .checked_mul(stride)
512                .ok_or_else(|| anyhow!("payload stride overflow for dims {dims:?}"))?;
513            max_offset = max_offset
514                .checked_add(span)
515                .ok_or_else(|| anyhow!("payload stride overflow for dims {dims:?}"))?;
516        }
517    }
518    if payload_len == 0 {
519        return Ok(0);
520    }
521    max_offset
522        .checked_add(1)
523        .ok_or_else(|| anyhow!("payload stride overflow for dims {dims:?}"))
524}
525
526fn logical_dims_from_axis_classes(payload_dims: &[usize], axis_classes: &[usize]) -> Vec<usize> {
527    axis_classes
528        .iter()
529        .map(|&class_id| payload_dims[class_id])
530        .collect()
531}
532
533fn col_major_multi_index(mut linear: usize, dims: &[usize]) -> Vec<usize> {
534    let mut index = Vec::with_capacity(dims.len());
535    for &dim in dims {
536        if dim == 0 {
537            index.push(0);
538        } else {
539            index.push(linear % dim);
540            linear /= dim;
541        }
542    }
543    index
544}
545
546fn offset_from_strides(index: &[usize], strides: &[isize]) -> Option<usize> {
547    index
548        .iter()
549        .zip(strides.iter())
550        .try_fold(0usize, |offset, (&value, &stride)| {
551            let stride = usize::try_from(stride).ok()?;
552            let term = value.checked_mul(stride)?;
553            offset.checked_add(term)
554        })
555}
556
557/// Structured tensor snapshot storage.
558///
559/// `data` and `strides` describe the payload tensor, while `axis_classes`
560/// describes how logical axes map onto payload axes. Logical flat-buffer
561/// semantics are column-major. A strided payload may contain unused backing
562/// gaps; reductions and nonfinite scans visit only entries addressed by the
563/// payload dimensions and strides.
564///
565/// A **dense** tensor has `axis_classes = [0, 1, ..., rank-1]` (each logical
566/// axis maps to a distinct payload axis). A **diagonal** tensor has
567/// `axis_classes = [0, 0, ..., 0]` (all logical axes share one payload axis),
568/// storing only the diagonal entries.
569///
570/// # Examples
571///
572/// ```
573/// use tensor4all_tensorbackend::StructuredStorage;
574///
575/// // Dense 2x3 storage, column-major: [[1,3,5],[2,4,6]]
576/// let dense = StructuredStorage::from_dense_col_major(
577///     vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3],
578/// ).unwrap();
579/// assert!(dense.is_dense());
580/// assert!(!dense.is_diag());
581/// assert_eq!(dense.logical_rank(), 2);
582/// assert_eq!(dense.logical_dims(), vec![2, 3]);
583///
584/// // Diagonal 3x3 storage
585/// let diag = StructuredStorage::from_diag_col_major(vec![1.0, 2.0, 3.0], 2).unwrap();
586/// assert!(diag.is_diag());
587/// assert_eq!(diag.logical_dims(), vec![3, 3]);
588/// assert_eq!(diag.len(), 3);
589/// ```
590#[derive(Debug, Clone, PartialEq)]
591pub struct StructuredStorage<T> {
592    data: Vec<T>,
593    payload_dims: Vec<usize>,
594    strides: Vec<isize>,
595    axis_classes: Vec<usize>,
596}
597
598impl<T> StructuredStorage<T> {
599    /// Creates a structured payload snapshot from explicit payload metadata.
600    ///
601    /// `payload_dims` and `strides` describe the compressed payload tensor,
602    /// while `axis_classes` maps logical axes onto payload axes in canonical
603    /// first-appearance order.
604    ///
605    /// # Errors
606    ///
607    /// Returns an error if:
608    /// - `axis_classes` is not in canonical first-appearance form
609    /// - `payload_dims` rank does not match `axis_classes`
610    /// - `strides` rank does not match `payload_dims`
611    /// - `data` length does not match the required storage length
612    ///
613    /// # Examples
614    ///
615    /// ```
616    /// use tensor4all_tensorbackend::StructuredStorage;
617    ///
618    /// // Dense 2x3 with explicit column-major strides
619    /// let s = StructuredStorage::new(
620    ///     vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
621    ///     vec![2, 3],     // payload_dims
622    ///     vec![1, 2],     // column-major strides
623    ///     vec![0, 1],     // axis_classes: each axis is independent
624    /// ).unwrap();
625    /// assert!(s.is_dense());
626    /// assert_eq!(s.len(), 6);
627    /// ```
628    pub fn new(
629        data: Vec<T>,
630        payload_dims: Vec<usize>,
631        strides: Vec<isize>,
632        axis_classes: Vec<usize>,
633    ) -> Result<Self> {
634        let required_len =
635            Storage::validate_structured_metadata(&payload_dims, &strides, &axis_classes)?;
636        ensure!(
637            data.len() == required_len,
638            "payload storage len {} does not match required len {} for dims {:?} and strides {:?}",
639            data.len(),
640            required_len,
641            payload_dims,
642            strides
643        );
644        Ok(Self {
645            data,
646            payload_dims,
647            strides,
648            axis_classes,
649        })
650    }
651
652    /// Creates a dense structured snapshot from column-major logical data.
653    ///
654    /// # Errors
655    ///
656    /// Returns [`StorageError::Operation`] when a column-major stride overflows
657    /// `usize` or when the payload length does not match `logical_dims`.
658    ///
659    /// # Examples
660    ///
661    /// ```
662    /// use tensor4all_tensorbackend::StructuredStorage;
663    ///
664    /// let s = StructuredStorage::from_dense_col_major(vec![10.0, 20.0, 30.0, 40.0], &[2, 2]).unwrap();
665    /// assert!(s.is_dense());
666    /// assert_eq!(s.data(), &[10.0, 20.0, 30.0, 40.0]);
667    /// ```
668    pub fn from_dense_col_major(data: Vec<T>, logical_dims: &[usize]) -> StorageResult<Self> {
669        let payload_dims = logical_dims.to_vec();
670        let strides = col_major_strides(&payload_dims)?;
671        let axis_classes = (0..logical_dims.len()).collect();
672        Self::new(data, payload_dims, strides, axis_classes).map_err(StorageError::from)
673    }
674
675    /// Creates a diagonal structured snapshot from column-major diagonal data.
676    ///
677    /// The resulting tensor has `logical_rank` axes, each of size `diag_data.len()`.
678    /// Only the diagonal entries are stored.
679    ///
680    /// # Errors
681    ///
682    /// Returns [`StorageError::Operation`] when a stride product overflows
683    /// `usize` or when the required payload length does not match
684    /// `diag_data.len()`.
685    ///
686    /// # Examples
687    ///
688    /// ```
689    /// use tensor4all_tensorbackend::StructuredStorage;
690    ///
691    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0, 3.0], 2).unwrap();
692    /// assert!(d.is_diag());
693    /// assert_eq!(d.logical_dims(), vec![3, 3]);
694    /// assert_eq!(d.data(), &[1.0, 2.0, 3.0]);
695    /// ```
696    pub fn from_diag_col_major(diag_data: Vec<T>, logical_rank: usize) -> StorageResult<Self> {
697        let payload_dims = if logical_rank == 0 {
698            vec![]
699        } else {
700            vec![diag_data.len()]
701        };
702        let strides = col_major_strides(&payload_dims)?;
703        let axis_classes = vec![0; logical_rank];
704        Self::new(diag_data, payload_dims, strides, axis_classes).map_err(StorageError::from)
705    }
706
707    /// Returns the payload data buffer as a slice.
708    ///
709    /// # Examples
710    ///
711    /// ```
712    /// use tensor4all_tensorbackend::StructuredStorage;
713    ///
714    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0], &[2]).unwrap();
715    /// assert_eq!(s.data(), &[1.0, 2.0]);
716    /// ```
717    pub fn data(&self) -> &[T] {
718        &self.data
719    }
720
721    /// Returns the payload tensor dimensions.
722    ///
723    /// For dense tensors, this equals the logical dimensions. For diagonal
724    /// tensors, this is a single-element slice `[n]` where `n` is the diagonal
725    /// length.
726    ///
727    /// # Examples
728    ///
729    /// ```
730    /// use tensor4all_tensorbackend::StructuredStorage;
731    ///
732    /// let s = StructuredStorage::from_dense_col_major(vec![0.0; 6], &[2, 3]).unwrap();
733    /// assert_eq!(s.payload_dims(), &[2, 3]);
734    ///
735    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 3).unwrap();
736    /// assert_eq!(d.payload_dims(), &[2]);
737    /// ```
738    pub fn payload_dims(&self) -> &[usize] {
739        &self.payload_dims
740    }
741
742    /// Returns the payload tensor strides.
743    ///
744    /// # Examples
745    ///
746    /// ```
747    /// use tensor4all_tensorbackend::StructuredStorage;
748    ///
749    /// // Column-major 2x3: strides are [1, 2]
750    /// let s = StructuredStorage::from_dense_col_major(vec![0.0; 6], &[2, 3]).unwrap();
751    /// assert_eq!(s.strides(), &[1, 2]);
752    /// ```
753    pub fn strides(&self) -> &[isize] {
754        &self.strides
755    }
756
757    /// Returns the canonical logical-to-payload axis classes.
758    ///
759    /// Each entry maps a logical axis to a payload axis index. Repeated values
760    /// indicate axes that share the same payload dimension (e.g., diagonal).
761    ///
762    /// # Examples
763    ///
764    /// ```
765    /// use tensor4all_tensorbackend::StructuredStorage;
766    ///
767    /// let dense = StructuredStorage::from_dense_col_major(vec![0.0; 4], &[2, 2]).unwrap();
768    /// assert_eq!(dense.axis_classes(), &[0, 1]);
769    ///
770    /// let diag = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
771    /// assert_eq!(diag.axis_classes(), &[0, 0]);
772    /// ```
773    pub fn axis_classes(&self) -> &[usize] {
774        &self.axis_classes
775    }
776
777    /// Returns the logical dimensions derived from `payload_dims` and `axis_classes`.
778    ///
779    /// # Examples
780    ///
781    /// ```
782    /// use tensor4all_tensorbackend::StructuredStorage;
783    ///
784    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0, 3.0], 3).unwrap();
785    /// assert_eq!(d.logical_dims(), vec![3, 3, 3]);
786    /// ```
787    pub fn logical_dims(&self) -> Vec<usize> {
788        logical_dims_from_axis_classes(&self.payload_dims, &self.axis_classes)
789    }
790
791    /// Returns the logical rank (number of logical axes).
792    ///
793    /// # Examples
794    ///
795    /// ```
796    /// use tensor4all_tensorbackend::StructuredStorage;
797    ///
798    /// let s = StructuredStorage::from_dense_col_major(vec![0.0; 6], &[2, 3]).unwrap();
799    /// assert_eq!(s.logical_rank(), 2);
800    /// ```
801    pub fn logical_rank(&self) -> usize {
802        self.axis_classes.len()
803    }
804
805    /// Returns `true` when the logical tensor is dense (each logical axis maps
806    /// to a unique payload axis).
807    ///
808    /// # Examples
809    ///
810    /// ```
811    /// use tensor4all_tensorbackend::StructuredStorage;
812    ///
813    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0], &[2]).unwrap();
814    /// assert!(s.is_dense());
815    ///
816    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
817    /// assert!(!d.is_dense());
818    /// ```
819    pub fn is_dense(&self) -> bool {
820        self.axis_classes
821            .iter()
822            .copied()
823            .eq(0..self.axis_classes.len())
824    }
825
826    /// Returns `true` when the logical tensor is diagonal (rank >= 2 and all
827    /// logical axes map to the same payload axis).
828    ///
829    /// # Examples
830    ///
831    /// ```
832    /// use tensor4all_tensorbackend::StructuredStorage;
833    ///
834    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
835    /// assert!(d.is_diag());
836    ///
837    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0], &[2]).unwrap();
838    /// assert!(!s.is_diag());
839    /// ```
840    pub fn is_diag(&self) -> bool {
841        self.logical_rank() >= 2 && self.axis_classes.iter().all(|&class_id| class_id == 0)
842    }
843
844    /// Returns the payload buffer length.
845    ///
846    /// # Examples
847    ///
848    /// ```
849    /// use tensor4all_tensorbackend::StructuredStorage;
850    ///
851    /// let dense = StructuredStorage::from_dense_col_major(vec![1.0, 2.0, 3.0], &[3]).unwrap();
852    /// assert_eq!(dense.len(), 3);
853    ///
854    /// let diag = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
855    /// assert_eq!(diag.len(), 2);
856    /// ```
857    pub fn len(&self) -> usize {
858        self.data.len()
859    }
860
861    /// Returns `true` when the payload buffer is empty.
862    ///
863    /// # Examples
864    ///
865    /// ```
866    /// use tensor4all_tensorbackend::StructuredStorage;
867    ///
868    /// let empty = StructuredStorage::from_dense_col_major(Vec::<f64>::new(), &[0]).unwrap();
869    /// assert!(empty.is_empty());
870    ///
871    /// let non_empty = StructuredStorage::from_dense_col_major(vec![1.0], &[1]).unwrap();
872    /// assert!(!non_empty.is_empty());
873    /// ```
874    pub fn is_empty(&self) -> bool {
875        self.data.is_empty()
876    }
877
878    /// Returns a borrowed view when the logical tensor is dense and the
879    /// payload is already stored contiguously in column-major order.
880    ///
881    /// Returns `None` for diagonal or non-contiguous payloads.
882    ///
883    /// # Examples
884    ///
885    /// ```
886    /// use tensor4all_tensorbackend::StructuredStorage;
887    ///
888    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0, 3.0], &[3]).unwrap();
889    /// assert_eq!(s.dense_col_major_view_if_contiguous(), Some(&[1.0, 2.0, 3.0][..]));
890    ///
891    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
892    /// assert_eq!(d.dense_col_major_view_if_contiguous(), None);
893    /// ```
894    pub fn dense_col_major_view_if_contiguous(&self) -> Option<&[T]> {
895        if self.is_dense()
896            && matches!(col_major_strides(&self.payload_dims), Ok(strides) if strides == self.strides)
897        {
898            Some(&self.data)
899        } else {
900            None
901        }
902    }
903
904    /// Returns a borrowed compact-payload view when the payload is already
905    /// stored contiguously in column-major order.
906    pub fn payload_col_major_view_if_contiguous(&self) -> Option<&[T]> {
907        if matches!(col_major_strides(&self.payload_dims), Ok(strides) if strides == self.strides) {
908            Some(&self.data)
909        } else {
910            None
911        }
912    }
913}
914
915impl<T: Clone> StructuredStorage<T> {
916    /// Materializes the payload tensor as a contiguous column-major buffer.
917    ///
918    /// If the payload is already column-major, returns a clone.
919    ///
920    /// # Examples
921    ///
922    /// ```
923    /// use tensor4all_tensorbackend::StructuredStorage;
924    ///
925    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]).unwrap();
926    /// assert_eq!(s.payload_col_major_vec(), vec![1.0, 2.0, 3.0, 4.0]);
927    /// ```
928    pub fn payload_col_major_vec(&self) -> Vec<T> {
929        let payload_len = self
930            .payload_dims
931            .iter()
932            .try_fold(1usize, |length, &dim| length.checked_mul(dim))
933            // `StructuredStorage::new` validates this invariant before any
934            // instance can be constructed; return an empty vector rather than
935            // wrapping if an invalid value ever reaches this private state.
936            .unwrap_or(0);
937        if payload_len == 0 {
938            return Vec::new();
939        }
940        if matches!(col_major_strides(&self.payload_dims), Ok(strides) if strides == self.strides) {
941            return self.data.clone();
942        }
943
944        let mut output = Vec::with_capacity(payload_len);
945        for linear in 0..payload_len {
946            let index = col_major_multi_index(linear, &self.payload_dims);
947            let Some(offset) = offset_from_strides(&index, &self.strides) else {
948                return Vec::new();
949            };
950            output.push(self.data[offset].clone());
951        }
952        output
953    }
954
955    /// Returns a copy of the storage with logical axes permuted.
956    ///
957    /// # Errors
958    ///
959    /// Returns [`StorageError::Operation`] when `perm` is not a permutation of
960    /// the logical axes.
961    ///
962    /// # Examples
963    ///
964    /// ```
965    /// use tensor4all_tensorbackend::StructuredStorage;
966    ///
967    /// // Diagonal 3x3x3 tensor; permute axes (identity for diag is always valid)
968    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0, 3.0], 3).unwrap();
969    /// let p = d.permute_logical_axes(&[2, 0, 1]).unwrap();
970    /// // Diagonal: all axes share the same dimension, so dims stay the same
971    /// assert_eq!(p.logical_dims(), vec![3, 3, 3]);
972    /// assert!(p.is_diag());
973    /// ```
974    pub fn permute_logical_axes(&self, perm: &[usize]) -> StorageResult<Self> {
975        if perm.len() != self.axis_classes.len() {
976            return Err(StorageError::from(anyhow::anyhow!(
977                "logical permutation length {} must match logical rank {}",
978                perm.len(),
979                self.axis_classes.len()
980            )));
981        }
982        let mut seen = vec![false; self.axis_classes.len()];
983        let axis_classes = perm
984            .iter()
985            .map(|&index| {
986                if index >= self.axis_classes.len() {
987                    return Err(anyhow::anyhow!(
988                        "logical permutation axis {index} is out of range for rank {}",
989                        self.axis_classes.len()
990                    ));
991                }
992                if seen[index] {
993                    return Err(anyhow::anyhow!("logical permutation repeats axis {index}"));
994                }
995                seen[index] = true;
996                Ok(self.axis_classes[index])
997            })
998            .collect::<Result<Vec<_>>>()
999            .map_err(StorageError::from)?;
1000        Self::new(
1001            self.data.clone(),
1002            self.payload_dims.clone(),
1003            self.strides.clone(),
1004            axis_classes,
1005        )
1006        .map_err(StorageError::from)
1007    }
1008}
1009
1010impl<T: Copy> StructuredStorage<T> {
1011    /// Wrap this payload as a compact [`Storage`] of the same scalar type.
1012    ///
1013    /// The payload keeps its dtype and metadata; nothing is widened or
1014    /// re-ordered. This is the construction seam used by dtype-generic storage
1015    /// algebra so each operation returns storage of its own scalar type.
1016    ///
1017    /// # Examples
1018    ///
1019    /// ```
1020    /// use tensor4all_tensorbackend::StructuredStorage;
1021    ///
1022    /// let payload = StructuredStorage::from_dense_col_major(vec![1.0_f32, 2.0], &[2]).unwrap();
1023    /// let storage = payload.into_storage();
1024    /// assert!(storage.is_f32());
1025    /// assert_eq!(storage.len(), 2);
1026    /// ```
1027    pub fn into_storage(self) -> Storage
1028    where
1029        T: StorageScalar,
1030    {
1031        T::wrap_payload(self)
1032    }
1033
1034    fn payload_scalar_at(&self, payload_coords: &[usize]) -> StorageResult<T> {
1035        if payload_coords.len() != self.payload_dims.len() {
1036            return Err(StorageError::InvalidStructuredStorage(format!(
1037                "payload coordinate rank {} does not match payload rank {}",
1038                payload_coords.len(),
1039                self.payload_dims.len()
1040            )));
1041        }
1042        let offset = payload_coords
1043            .iter()
1044            .zip(self.payload_dims.iter())
1045            .zip(self.strides.iter())
1046            .try_fold(0usize, |offset, ((&coordinate, &dim), &stride)| {
1047                if coordinate >= dim {
1048                    return Err(StorageError::InvalidStructuredStorage(format!(
1049                        "payload coordinate {coordinate} is out of bounds for dim {dim}"
1050                    )));
1051                }
1052                let stride = usize::try_from(stride).map_err(|_| {
1053                    StorageError::InvalidStructuredStorage("negative stride".into())
1054                })?;
1055                let term = coordinate.checked_mul(stride).ok_or_else(|| {
1056                    StorageError::InvalidStructuredStorage("payload offset overflow".into())
1057                })?;
1058                offset.checked_add(term).ok_or_else(|| {
1059                    StorageError::InvalidStructuredStorage("payload offset overflow".into())
1060                })
1061            })?;
1062        self.data.get(offset).copied().ok_or_else(|| {
1063            StorageError::InvalidStructuredStorage("payload offset outside storage".into())
1064        })
1065    }
1066
1067    /// Maps payload elements while preserving payload metadata and axis classes.
1068    ///
1069    /// # Examples
1070    ///
1071    /// ```
1072    /// use tensor4all_tensorbackend::StructuredStorage;
1073    ///
1074    /// let s = StructuredStorage::from_dense_col_major(vec![1.0, 2.0, 3.0], &[3]).unwrap();
1075    /// let doubled = s.map_copy(|x| x * 2.0);
1076    /// assert_eq!(doubled.data(), &[2.0, 4.0, 6.0]);
1077    /// ```
1078    pub fn map_copy<U>(&self, mut f: impl FnMut(T) -> U) -> StructuredStorage<U> {
1079        StructuredStorage {
1080            data: self.data.iter().copied().map(&mut f).collect(),
1081            payload_dims: self.payload_dims.clone(),
1082            strides: self.strides.clone(),
1083            axis_classes: self.axis_classes.clone(),
1084        }
1085    }
1086}
1087
1088impl<T: Copy + Default> StructuredStorage<T> {
1089    /// Returns the checked product of the logical dimensions, rejecting
1090    /// overflow. The fallible dense exporters rely on this so that a logical
1091    /// product that overflows `usize` fails closed instead of being treated
1092    /// as an empty tensor.
1093    fn checked_logical_len(&self) -> StorageResult<usize> {
1094        let dims = self.logical_dims();
1095        dims.iter()
1096            .try_fold(1usize, |length, &dim| length.checked_mul(dim))
1097            .ok_or_else(|| {
1098                StorageError::InvalidStructuredStorage(format!(
1099                    "logical dims product overflow for {dims:?}"
1100                ))
1101            })
1102    }
1103
1104    /// Materializes the logical tensor as a contiguous column-major dense buffer.
1105    ///
1106    /// Repeated entries in `axis_classes` encode equality constraints between
1107    /// logical axes. Logical indices that violate those constraints are
1108    /// structural zeros in the dense materialization.
1109    ///
1110    /// # Errors
1111    ///
1112    /// Returns [`StorageError::InvalidStructuredStorage`] if the logical
1113    /// dimension product overflows `usize`.
1114    ///
1115    /// # Examples
1116    ///
1117    /// ```
1118    /// use tensor4all_tensorbackend::StructuredStorage;
1119    ///
1120    /// // Diagonal [1, 2] in 2x2 becomes [1, 0, 0, 2] column-major
1121    /// let d = StructuredStorage::from_diag_col_major(vec![1.0, 2.0], 2).unwrap();
1122    /// assert_eq!(d.logical_dense_col_major_vec().unwrap(), vec![1.0, 0.0, 0.0, 2.0]);
1123    /// ```
1124    pub fn logical_dense_col_major_vec(&self) -> StorageResult<Vec<T>> {
1125        let logical_dims = self.logical_dims();
1126        let logical_len = self.checked_logical_len()?;
1127        if logical_len == 0 {
1128            return Ok(Vec::new());
1129        }
1130        if let Some(view) = self.dense_col_major_view_if_contiguous() {
1131            return Ok(view.to_vec());
1132        }
1133        if self.is_dense() {
1134            return Ok(self.payload_col_major_vec());
1135        }
1136
1137        let mut payload_index = vec![0usize; self.payload_dims.len()];
1138        Ok((0..logical_len)
1139            .map(|linear| {
1140                let logical_index = col_major_multi_index(linear, &logical_dims);
1141                self.value_at_with_dims(&logical_index, &logical_dims, &mut payload_index)
1142                    .unwrap_or_default()
1143            })
1144            .collect())
1145    }
1146
1147    fn for_each_payload_value(&self, mut f: impl FnMut(T)) {
1148        if let Some(view) = self.payload_col_major_view_if_contiguous() {
1149            for &value in view {
1150                f(value);
1151            }
1152            return;
1153        }
1154        let payload_len = self
1155            .payload_dims
1156            .iter()
1157            .try_fold(1usize, |length, &dim| length.checked_mul(dim))
1158            .unwrap_or(0);
1159        let mut payload_index = vec![0usize; self.payload_dims.len()];
1160        for _ in 0..payload_len {
1161            let Some(offset) = offset_from_strides(&payload_index, &self.strides) else {
1162                return;
1163            };
1164            f(self.data[offset]);
1165            let mut carry = true;
1166            for (coordinate, &dim) in payload_index.iter_mut().zip(self.payload_dims.iter()) {
1167                if !carry {
1168                    break;
1169                }
1170                *coordinate += 1;
1171                if *coordinate == dim {
1172                    *coordinate = 0;
1173                } else {
1174                    carry = false;
1175                }
1176            }
1177        }
1178    }
1179
1180    fn value_at_with_dims(
1181        &self,
1182        logical_index: &[usize],
1183        logical_dims: &[usize],
1184        payload_index: &mut [usize],
1185    ) -> StorageResult<T> {
1186        if logical_index.len() != logical_dims.len() {
1187            return Err(StorageError::InvalidStructuredStorage(format!(
1188                "logical index rank {} does not match rank {}",
1189                logical_index.len(),
1190                logical_dims.len()
1191            )));
1192        }
1193        if payload_index.len() < self.payload_dims.len() {
1194            return Err(StorageError::InvalidStructuredStorage(format!(
1195                "payload scratch rank {} is smaller than {}",
1196                payload_index.len(),
1197                self.payload_dims.len()
1198            )));
1199        }
1200        payload_index[..self.payload_dims.len()].fill(usize::MAX);
1201        for ((&value, &dim), &class_id) in logical_index
1202            .iter()
1203            .zip(logical_dims.iter())
1204            .zip(self.axis_classes.iter())
1205        {
1206            if value >= dim {
1207                return Err(StorageError::InvalidStructuredStorage(format!(
1208                    "logical index {value} is out of bounds for dim {dim}"
1209                )));
1210            }
1211            if payload_index[class_id] == usize::MAX {
1212                payload_index[class_id] = value;
1213            } else if payload_index[class_id] != value {
1214                return Ok(T::default());
1215            }
1216        }
1217        let offset = payload_index[..self.payload_dims.len()]
1218            .iter()
1219            .zip(self.strides.iter())
1220            .try_fold(0usize, |offset, (&value, &stride)| {
1221                let stride = usize::try_from(stride).map_err(|_| {
1222                    StorageError::InvalidStructuredStorage("negative stride".into())
1223                })?;
1224                let term = value.checked_mul(stride).ok_or_else(|| {
1225                    StorageError::InvalidStructuredStorage("payload offset overflow".into())
1226                })?;
1227                offset.checked_add(term).ok_or_else(|| {
1228                    StorageError::InvalidStructuredStorage("payload offset overflow".into())
1229                })
1230            })?;
1231        self.data.get(offset).copied().ok_or_else(|| {
1232            StorageError::InvalidStructuredStorage("payload offset outside storage".into())
1233        })
1234    }
1235}
1236
1237/// Storage backend for tensor data.
1238///
1239/// Public callers interact with this opaque wrapper through constructors and
1240/// high-level query/materialization methods.
1241///
1242/// # Examples
1243///
1244/// ```
1245/// use tensor4all_tensorbackend::Storage;
1246///
1247/// // Dense 2x3 matrix stored column-major: [[1,2,3],[4,5,6]]
1248/// let data = vec![1.0_f64, 4.0, 2.0, 5.0, 3.0, 6.0];
1249/// let s = Storage::from_dense_col_major(data, &[2, 3]).unwrap();
1250/// assert!(s.is_f64());
1251/// assert!(!s.is_complex());
1252///
1253/// // Diagonal storage: 2x2 identity-like diagonal
1254/// let diag = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
1255/// assert!(diag.is_f64());
1256/// ```
1257#[derive(Debug, Clone)]
1258pub struct Storage(pub(crate) StorageRepr);
1259
1260/// Classifies the compact layout used by [`Storage`].
1261///
1262/// Use this to distinguish dense logical payloads from diagonal/copy payloads
1263/// and general structured payloads without exposing the internal storage enum.
1264///
1265/// # Examples
1266///
1267/// ```
1268/// use tensor4all_tensorbackend::{Storage, StorageKind};
1269///
1270/// let dense = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
1271/// assert_eq!(dense.storage_kind(), StorageKind::Dense);
1272///
1273/// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1274/// assert_eq!(diag.storage_kind(), StorageKind::Diagonal);
1275/// ```
1276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1277pub enum StorageKind {
1278    /// Logical dense payload layout.
1279    Dense,
1280    /// Diagonal or copy-tensor payload layout.
1281    Diagonal,
1282    /// General structured payload layout with repeated axis classes.
1283    Structured,
1284}
1285
1286/// Errors returned by storage payload and elementwise operations.
1287///
1288/// Use this to distinguish scalar-kind mismatches, length mismatches, and
1289/// invalid structured-storage metadata from general backend failures.
1290///
1291/// # Examples
1292///
1293/// ```
1294/// use tensor4all_tensorbackend::{Storage, StorageError};
1295///
1296/// let storage = Storage::from_dense_col_major(vec![1.0_f64], &[1]).unwrap();
1297/// let err = storage.payload_c64_col_major_vec().unwrap_err();
1298/// assert!(matches!(err, StorageError::ScalarKindMismatch { .. }));
1299/// ```
1300#[derive(Debug, thiserror::Error)]
1301pub enum StorageError {
1302    /// The storage scalar kind did not match the requested operation.
1303    #[error("expected {expected} storage when {operation}, got {actual}")]
1304    ScalarKindMismatch {
1305        /// The scalar kind that the caller requested.
1306        expected: &'static str,
1307        /// The scalar kind actually stored.
1308        actual: &'static str,
1309        /// Human-readable operation description.
1310        operation: &'static str,
1311    },
1312    /// Two storages had different payload lengths for an elementwise operation.
1313    #[error("storage lengths must match for {operation}: {left} != {right}")]
1314    LengthMismatch {
1315        /// Name of the operation being performed.
1316        operation: &'static str,
1317        /// Left-hand payload length.
1318        left: usize,
1319        /// Right-hand payload length.
1320        right: usize,
1321    },
1322    /// Structured storage metadata was invalid after an operation.
1323    #[error("invalid structured storage: {0}")]
1324    InvalidStructuredStorage(String),
1325    /// The requested operation does not support the provided storage kinds.
1326    #[error("storage types are not supported for {operation}: {left} vs {right}")]
1327    OperationNotSupported {
1328        /// Name of the operation being performed.
1329        operation: &'static str,
1330        /// Left-hand storage kind.
1331        left: &'static str,
1332        /// Right-hand storage kind.
1333        right: &'static str,
1334    },
1335    /// A storage operation failed with an internal diagnostic.
1336    #[error("storage operation failed: {source}")]
1337    Operation {
1338        /// Original internal diagnostic, preserving its source chain.
1339        #[source]
1340        source: anyhow::Error,
1341    },
1342}
1343
1344impl From<anyhow::Error> for StorageError {
1345    fn from(source: anyhow::Error) -> Self {
1346        Self::Operation { source }
1347    }
1348}
1349
1350/// Result type returned by storage methods that can fail with [`StorageError`].
1351pub type StorageResult<T> = std::result::Result<T, StorageError>;
1352
1353#[derive(Debug, Clone)]
1354pub(crate) enum StorageRepr {
1355    /// Storage with f32 elements.
1356    F32(StructuredStorage<f32>),
1357    /// Storage with f64 elements.
1358    F64(StructuredStorage<f64>),
1359    /// Storage with Complex32 elements.
1360    C32(StructuredStorage<Complex32>),
1361    /// Storage with Complex64 elements.
1362    C64(StructuredStorage<Complex64>),
1363}
1364
1365impl StorageRepr {
1366    fn dtype(&self) -> DType {
1367        storage_payload_dispatch!(self, Payload => |_payload| <Payload as TensorScalar>::dtype())
1368    }
1369
1370    fn scalar_kind(&self) -> &'static str {
1371        storage_payload_dispatch!(self, Payload => |_payload| <Payload as StorageScalar>::KIND)
1372    }
1373
1374    fn is_complex(&self) -> bool {
1375        storage_payload_dispatch!(self, Payload => |_payload| <Payload as StorageScalar>::IS_COMPLEX)
1376    }
1377}
1378
1379/// Types that can be computed as the result of a reduction over `Storage`.
1380///
1381/// This lets callers write `let s: T = tensor.sum();` without matching on
1382/// the storage variant. Implemented for `f64`, `Complex64`, `f32`, and
1383/// `Complex32`; every reduction widens compact payload values to the requested
1384/// accumulator, so a narrow payload can be summed without changing its stored
1385/// dtype.
1386///
1387/// # Examples
1388///
1389/// ```
1390/// use tensor4all_tensorbackend::{Storage, SumFromStorage};
1391///
1392/// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0], &[3]).unwrap();
1393/// let total: f64 = f64::sum_from_storage(&s);
1394/// assert!((total - 6.0).abs() < 1e-10);
1395///
1396/// let narrow = Storage::from_dense_col_major(vec![1.0_f32, 2.0], &[2]).unwrap();
1397/// assert_eq!(f32::sum_from_storage(&narrow), 3.0_f32);
1398/// assert_eq!(f64::sum_from_storage(&narrow), 3.0_f64);
1399/// ```
1400pub trait SumFromStorage: Sized {
1401    /// Compute the sum of all elements in the storage.
1402    fn sum_from_storage(storage: &Storage) -> Self;
1403}
1404
1405impl SumFromStorage for f64 {
1406    fn sum_from_storage(storage: &Storage) -> Self {
1407        let mut sum = 0.0_f64;
1408        storage.for_each_payload_parts(|re, _im| sum += re);
1409        sum
1410    }
1411}
1412
1413impl SumFromStorage for Complex64 {
1414    fn sum_from_storage(storage: &Storage) -> Self {
1415        let mut sum = Complex64::new(0.0, 0.0);
1416        storage.for_each_payload_parts(|re, im| sum += Complex64::new(re, im));
1417        sum
1418    }
1419}
1420
1421impl SumFromStorage for f32 {
1422    fn sum_from_storage(storage: &Storage) -> Self {
1423        let mut sum = 0.0_f32;
1424        storage.for_each_payload_parts(|re, _im| sum += re as f32);
1425        sum
1426    }
1427}
1428
1429impl SumFromStorage for Complex32 {
1430    fn sum_from_storage(storage: &Storage) -> Self {
1431        let mut sum = Complex32::new(0.0, 0.0);
1432        storage.for_each_payload_parts(|re, im| sum += Complex32::new(re as f32, im as f32));
1433        sum
1434    }
1435}
1436
1437/// Lattice width of a floating dtype, or `None` for non-floating dtypes.
1438fn float_lattice_width(dtype: DType) -> Option<u8> {
1439    match dtype {
1440        DType::F32 | DType::C32 => Some(32),
1441        DType::F64 | DType::C64 => Some(64),
1442        _ => None,
1443    }
1444}
1445
1446/// Least upper bound dtype of a set of floating dtypes.
1447///
1448/// The lattice is `f32 < f64`, `Complex32 < Complex64`, and real `<` complex, so
1449/// the result takes the widest real component and the complex component as soon
1450/// as any input is complex. Returns `None` when an input is not floating point.
1451fn promote_float_dtypes(dtypes: impl IntoIterator<Item = DType>) -> Option<DType> {
1452    let mut width = 0_u8;
1453    let mut complex = false;
1454    for dtype in dtypes {
1455        width = width.max(float_lattice_width(dtype)?);
1456        complex |= matches!(dtype, DType::C32 | DType::C64);
1457    }
1458    match (width, complex) {
1459        (32, false) => Some(DType::F32),
1460        (64, false) => Some(DType::F64),
1461        (32, true) => Some(DType::C32),
1462        (64, true) => Some(DType::C64),
1463        _ => None,
1464    }
1465}
1466
1467/// Rebuild a compact payload from copied metadata and replacement values.
1468///
1469/// The metadata comes from an already validated payload, so this construction
1470/// cannot introduce invalid strides or lengths; it is used by the infallible
1471/// elementwise operators.
1472fn rebuild_payload<T: StorageScalar>(
1473    payload_dims: &[usize],
1474    strides: &[isize],
1475    axis_classes: &[usize],
1476    data: Vec<T>,
1477) -> Storage {
1478    T::wrap_payload(StructuredStorage {
1479        data,
1480        payload_dims: payload_dims.to_vec(),
1481        strides: strides.to_vec(),
1482        axis_classes: axis_classes.to_vec(),
1483    })
1484}
1485
1486/// Zip two same-scalar payloads, preserving the left payload's metadata.
1487fn zip_payloads<T, U>(
1488    left: &StructuredStorage<T>,
1489    right: &StructuredStorage<T>,
1490    operation: &'static str,
1491    mut combine: impl FnMut(T, T) -> U,
1492) -> StorageResult<Storage>
1493where
1494    T: StorageScalar,
1495    U: StorageScalar,
1496{
1497    if left.len() != right.len() {
1498        return Err(StorageError::LengthMismatch {
1499            operation,
1500            left: left.len(),
1501            right: right.len(),
1502        });
1503    }
1504    let values: Vec<U> = left
1505        .data()
1506        .iter()
1507        .copied()
1508        .zip(right.data().iter().copied())
1509        .map(|(x, y)| combine(x, y))
1510        .collect();
1511    StructuredStorage::new(
1512        values,
1513        left.payload_dims().to_vec(),
1514        left.strides().to_vec(),
1515        left.axis_classes().to_vec(),
1516    )
1517    .map(StructuredStorage::into_storage)
1518    .map_err(StorageError::from)
1519}
1520
1521/// Materialize the logical dense values of one payload, checking the shape.
1522fn checked_dense_payload<T: StorageScalar>(
1523    payload: &StructuredStorage<T>,
1524    logical_dims: &[usize],
1525) -> StorageResult<Vec<T>> {
1526    let structured_dims = payload.logical_dims();
1527    if structured_dims != logical_dims {
1528        return Err(StorageError::InvalidStructuredStorage(format!(
1529            "logical dims {:?} do not match stored logical dims {:?}",
1530            logical_dims, structured_dims
1531        )));
1532    }
1533    payload.logical_dense_col_major_vec()
1534}
1535
1536impl Storage {
1537    #[cfg(test)]
1538    pub(crate) fn from_repr(repr: StorageRepr) -> Self {
1539        Self(repr)
1540    }
1541
1542    /// Visit every referenced compact payload value as widened `(real, imaginary)` parts.
1543    ///
1544    /// Stride gaps and padding in a structured backing buffer are not tensor
1545    /// values and are excluded. This is the single dtype-agnostic payload scan:
1546    /// reductions and elementwise algebra are written once against it.
1547    fn for_each_payload_parts(&self, mut visit: impl FnMut(f64, f64)) {
1548        storage_payload_dispatch!(&self.0, Payload => |payload| {
1549            payload.for_each_payload_value(|value| {
1550                let (re, im) = <Payload as StorageScalar>::widened_parts(value);
1551                visit(re, im);
1552            })
1553        })
1554    }
1555
1556    /// Visit every referenced compact payload entry's magnitude as `f64`.
1557    ///
1558    /// Real payloads visit `|x|`; complex payloads visit `hypot(re, im)`, and a
1559    /// NaN component propagates as `NaN` rather than being masked by the other
1560    /// component. Narrow payloads widen per value for the scan only.
1561    fn for_each_payload_magnitude(&self, mut visit: impl FnMut(f64)) {
1562        storage_payload_dispatch!(&self.0, Payload => |payload| {
1563            payload.for_each_payload_value(|value| {
1564                let (re, im) = <Payload as StorageScalar>::widened_parts(value);
1565                visit(if <Payload as StorageScalar>::IS_COMPLEX {
1566                    if re.is_nan() || im.is_nan() {
1567                        f64::NAN
1568                    } else {
1569                        re.hypot(im)
1570                    }
1571                } else {
1572                    re.abs()
1573                });
1574            })
1575        })
1576    }
1577
1578    fn invalid_storage_error(err: anyhow::Error) -> StorageError {
1579        StorageError::from(err)
1580    }
1581
1582    #[cfg(test)]
1583    pub(crate) fn repr(&self) -> &StorageRepr {
1584        &self.0
1585    }
1586
1587    fn validate_dense_len<T>(data: &[T], logical_dims: &[usize], label: &str) -> Result<()> {
1588        let expected_len = logical_dims.iter().try_fold(1usize, |length, &dim| {
1589            length
1590                .checked_mul(dim)
1591                .ok_or_else(|| anyhow!("{label} dimension product overflows for {logical_dims:?}"))
1592        })?;
1593        ensure!(
1594            data.len() == expected_len,
1595            "{label} len {} does not match logical dims {:?} (expected {})",
1596            data.len(),
1597            logical_dims,
1598            expected_len
1599        );
1600        Ok(())
1601    }
1602
1603    /// Create dense storage from column-major logical values (generic over scalar type).
1604    ///
1605    /// The scalar type is inferred from the `data` argument.
1606    ///
1607    /// # Errors
1608    ///
1609    /// Returns an error when the shape product would overflow `usize` or
1610    /// `data.len()` does not match the expected shape.
1611    ///
1612    /// # Examples
1613    ///
1614    /// ```
1615    /// use tensor4all_tensorbackend::Storage;
1616    ///
1617    /// // 2x2 matrix, column-major: [[1,3],[2,4]]
1618    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0, 4.0], &[2, 2]).unwrap();
1619    /// assert!(s.is_f64());
1620    /// assert!(s.is_dense());
1621    /// assert_eq!(s.len(), 4);
1622    /// ```
1623    pub fn from_dense_col_major<T: StorageScalar>(
1624        data: Vec<T>,
1625        logical_dims: &[usize],
1626    ) -> Result<Self> {
1627        T::build_dense_storage(data, logical_dims)
1628    }
1629
1630    /// Create diagonal storage from column-major diagonal payload values (generic over scalar type).
1631    ///
1632    /// Creates a rank-2 diagonal storage by default. The scalar type is
1633    /// inferred from `diag_data`.
1634    ///
1635    /// # Errors
1636    ///
1637    /// Returns an error when the diagonal payload length does not match the
1638    /// length required for `logical_rank` or a stride product would overflow
1639    /// `usize`.
1640    ///
1641    /// # Examples
1642    ///
1643    /// ```
1644    /// use tensor4all_tensorbackend::Storage;
1645    ///
1646    /// let s = Storage::from_diag_col_major(vec![1.0_f64, 2.0, 3.0], 2).unwrap();
1647    /// assert!(s.is_diag());
1648    /// assert!(s.is_f64());
1649    /// assert_eq!(s.len(), 3);
1650    /// ```
1651    pub fn from_diag_col_major<T: StorageScalar>(
1652        diag_data: Vec<T>,
1653        logical_rank: usize,
1654    ) -> Result<Self> {
1655        T::build_diag_storage(diag_data, logical_rank)
1656    }
1657
1658    /// Create a new 1D zero-initialized dense storage (generic over scalar type).
1659    ///
1660    /// # Examples
1661    ///
1662    /// ```
1663    /// use tensor4all_tensorbackend::Storage;
1664    ///
1665    /// let s = Storage::new_dense::<f64>(5).unwrap();
1666    /// assert!(s.is_dense());
1667    /// assert_eq!(s.len(), 5);
1668    /// assert!((s.max_abs()).abs() < 1e-10);
1669    /// ```
1670    ///
1671    /// # Errors
1672    ///
1673    /// Returns [`StorageError::Operation`] when the dense storage construction
1674    /// or allocation fails.
1675    pub fn new_dense<T: StorageScalar + Default>(size: usize) -> StorageResult<Self> {
1676        Self::from_dense_col_major(vec![T::default(); size], &[size])
1677            .map_err(Self::invalid_storage_error)
1678    }
1679
1680    /// Create a new diagonal storage with the given diagonal data (generic over scalar type).
1681    ///
1682    /// # Errors
1683    ///
1684    /// Returns an error if diagonal metadata is invalid.
1685    ///
1686    /// # Examples
1687    ///
1688    /// ```
1689    /// use tensor4all_tensorbackend::Storage;
1690    ///
1691    /// let s = Storage::new_diag(vec![1.0_f64, 2.0, 3.0]).unwrap();
1692    /// assert!(s.is_diag());
1693    /// assert!(s.is_f64());
1694    /// ```
1695    pub fn new_diag<T: StorageScalar>(diag_data: Vec<T>) -> StorageResult<Self> {
1696        Self::from_diag_col_major(diag_data, 2).map_err(Self::invalid_storage_error)
1697    }
1698
1699    /// Create a new structured storage (generic over scalar type).
1700    ///
1701    /// # Errors
1702    ///
1703    /// Returns an error when `axis_classes` is not in canonical
1704    /// first-appearance order, when the `payload_dims` or `strides` ranks do
1705    /// not match `axis_classes`, when a stride is negative, when a size
1706    /// product would overflow `usize`, or when `data.len()` does not match the
1707    /// required payload length.
1708    ///
1709    /// # Examples
1710    ///
1711    /// ```
1712    /// use tensor4all_tensorbackend::Storage;
1713    ///
1714    /// // Diagonal-like structured storage: axis_classes = [0, 0]
1715    /// let s = Storage::new_structured(
1716    ///     vec![1.0_f64, 2.0],
1717    ///     vec![2],         // payload_dims
1718    ///     vec![1],         // strides
1719    ///     vec![0, 0],      // axis_classes: both axes map to payload axis 0
1720    /// ).unwrap();
1721    /// assert!(s.is_diag());
1722    /// ```
1723    pub fn new_structured<T: StorageScalar>(
1724        data: Vec<T>,
1725        payload_dims: Vec<usize>,
1726        strides: Vec<isize>,
1727        axis_classes: Vec<usize>,
1728    ) -> Result<Self> {
1729        T::build_structured_storage(data, payload_dims, strides, axis_classes)
1730    }
1731
1732    /// Validate structured-storage metadata and return the required payload length.
1733    ///
1734    /// The metadata must be internally consistent: canonical axis classes, a
1735    /// payload rank implied by the axis classes, matching dim/stride ranks,
1736    /// non-negative strides, and size products that fit in `usize`. The
1737    /// returned length is exactly what a constructed [`Storage`] would
1738    /// require, so untrusted payload lengths can be rejected before any
1739    /// allocation.
1740    ///
1741    /// # Errors
1742    ///
1743    /// Returns an error if the metadata is inconsistent or its size products
1744    /// overflow `usize`.
1745    ///
1746    /// # Examples
1747    ///
1748    /// ```
1749    /// use tensor4all_tensorbackend::Storage;
1750    ///
1751    /// let required = Storage::validate_structured_metadata(
1752    ///     &[2],
1753    ///     &[1],
1754    ///     &[0, 0],
1755    /// ).unwrap();
1756    /// assert_eq!(required, 2);
1757    ///
1758    /// assert!(Storage::validate_structured_metadata(&[2], &[-1], &[0]).is_err());
1759    /// ```
1760    pub fn validate_structured_metadata(
1761        payload_dims: &[usize],
1762        strides: &[isize],
1763        axis_classes: &[usize],
1764    ) -> Result<usize> {
1765        validate_canonical_axis_classes(axis_classes)?;
1766        let payload_rank = axis_classes.iter().try_fold(0usize, |rank, &class_id| {
1767            let required_rank = class_id
1768                .checked_add(1)
1769                .ok_or_else(|| anyhow!("axis class index overflow"))?;
1770            Ok::<_, anyhow::Error>(rank.max(required_rank))
1771        })?;
1772        ensure!(
1773            payload_dims.len() == payload_rank,
1774            "payload rank {} does not match axis_classes {:?}",
1775            payload_dims.len(),
1776            axis_classes
1777        );
1778        ensure!(
1779            strides.len() == payload_dims.len(),
1780            "payload dims {:?} and strides {:?} must have the same rank",
1781            payload_dims,
1782            strides
1783        );
1784        required_storage_len(payload_dims, strides)
1785    }
1786
1787    /// Check if this storage is logically dense.
1788    ///
1789    /// # Examples
1790    ///
1791    /// ```
1792    /// use tensor4all_tensorbackend::Storage;
1793    ///
1794    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
1795    /// assert!(s.is_dense());
1796    ///
1797    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
1798    /// assert!(!d.is_dense());
1799    /// ```
1800    pub fn is_dense(&self) -> bool {
1801        storage_payload_dispatch!(&self.0, Payload => |value| value.is_dense())
1802    }
1803
1804    /// Check if this storage is a Diag storage type.
1805    ///
1806    /// # Examples
1807    ///
1808    /// ```
1809    /// use tensor4all_tensorbackend::Storage;
1810    ///
1811    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
1812    /// assert!(d.is_diag());
1813    /// ```
1814    pub fn is_diag(&self) -> bool {
1815        storage_payload_dispatch!(&self.0, Payload => |value| value.is_diag())
1816    }
1817
1818    /// Returns the compact layout class for this storage.
1819    ///
1820    /// The return value is metadata-only and never materializes dense logical
1821    /// values. Use it to choose whether to read compact payload metadata or
1822    /// dense logical values.
1823    ///
1824    /// # Examples
1825    ///
1826    /// ```
1827    /// use tensor4all_tensorbackend::{Storage, StorageKind};
1828    ///
1829    /// let structured = Storage::new_structured(
1830    ///     vec![1.0_f64, 2.0],
1831    ///     vec![2],
1832    ///     vec![1],
1833    ///     vec![0, 0],
1834    /// ).unwrap();
1835    /// assert_eq!(structured.storage_kind(), StorageKind::Diagonal);
1836    /// ```
1837    pub fn storage_kind(&self) -> StorageKind {
1838        if self.is_dense() {
1839            StorageKind::Dense
1840        } else if self.is_diag() {
1841            StorageKind::Diagonal
1842        } else {
1843            StorageKind::Structured
1844        }
1845    }
1846
1847    /// Returns the logical tensor dimensions represented by this storage.
1848    ///
1849    /// The dimensions are derived from payload dimensions and `axis_classes`.
1850    ///
1851    /// # Examples
1852    ///
1853    /// ```
1854    /// use tensor4all_tensorbackend::Storage;
1855    ///
1856    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1857    /// assert_eq!(diag.logical_dims(), vec![2, 2]);
1858    /// ```
1859    pub fn logical_dims(&self) -> Vec<usize> {
1860        storage_payload_dispatch!(&self.0, Payload => |value| value.logical_dims())
1861    }
1862
1863    /// Returns the logical tensor rank represented by this storage.
1864    ///
1865    /// This equals `axis_classes().len()`, not necessarily `payload_dims().len()`.
1866    ///
1867    /// # Examples
1868    ///
1869    /// ```
1870    /// use tensor4all_tensorbackend::Storage;
1871    ///
1872    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 3).unwrap();
1873    /// assert_eq!(diag.logical_rank(), 3);
1874    /// assert_eq!(diag.payload_dims(), &[2]);
1875    /// ```
1876    pub fn logical_rank(&self) -> usize {
1877        storage_payload_dispatch!(&self.0, Payload => |value| value.logical_rank())
1878    }
1879
1880    /// Returns the compact payload dimensions.
1881    ///
1882    /// For dense storage these match logical dimensions. For diagonal storage
1883    /// this is rank-1 even when the logical tensor has multiple axes.
1884    ///
1885    /// # Examples
1886    ///
1887    /// ```
1888    /// use tensor4all_tensorbackend::Storage;
1889    ///
1890    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1891    /// assert_eq!(diag.payload_dims(), &[2]);
1892    /// ```
1893    pub fn payload_dims(&self) -> &[usize] {
1894        storage_payload_dispatch!(&self.0, Payload => |value| value.payload_dims())
1895    }
1896
1897    /// Returns the compact payload strides.
1898    ///
1899    /// Strides are measured in stored scalar elements and describe the compact
1900    /// payload buffer, not the logical dense tensor.
1901    ///
1902    /// # Examples
1903    ///
1904    /// ```
1905    /// use tensor4all_tensorbackend::Storage;
1906    ///
1907    /// let dense = Storage::from_dense_col_major(vec![0.0_f64; 6], &[2, 3]).unwrap();
1908    /// assert_eq!(dense.payload_strides(), &[1, 2]);
1909    /// ```
1910    pub fn payload_strides(&self) -> &[isize] {
1911        storage_payload_dispatch!(&self.0, Payload => |value| value.strides())
1912    }
1913
1914    /// Returns logical-axis equivalence classes for this storage.
1915    ///
1916    /// Repeated class labels mean the corresponding logical axes share one
1917    /// payload axis. Dense storage has `[0, 1, ...]`; diagonal storage has
1918    /// repeated zero labels.
1919    ///
1920    /// # Examples
1921    ///
1922    /// ```
1923    /// use tensor4all_tensorbackend::Storage;
1924    ///
1925    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1926    /// assert_eq!(diag.axis_classes(), &[0, 0]);
1927    /// ```
1928    pub fn axis_classes(&self) -> &[usize] {
1929        storage_payload_dispatch!(&self.0, Payload => |value| value.axis_classes())
1930    }
1931
1932    /// Returns the number of stored compact payload elements.
1933    ///
1934    /// For dense storage this equals the logical dense length. For diagonal and
1935    /// structured storage this is the compact payload length.
1936    ///
1937    /// # Examples
1938    ///
1939    /// ```
1940    /// use tensor4all_tensorbackend::Storage;
1941    ///
1942    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1943    /// assert_eq!(diag.payload_len(), 2);
1944    /// ```
1945    pub fn payload_len(&self) -> usize {
1946        self.len()
1947    }
1948
1949    /// Copies the compact `f32` payload in column-major payload order.
1950    ///
1951    /// This does not materialize logical dense values and never widens: a
1952    /// narrow payload stays `f32`.
1953    ///
1954    /// # Errors
1955    ///
1956    /// Returns [`StorageError::ScalarKindMismatch`] if the storage scalar type
1957    /// is not `f32`.
1958    ///
1959    /// # Examples
1960    ///
1961    /// ```
1962    /// use tensor4all_tensorbackend::Storage;
1963    ///
1964    /// let diag = Storage::from_diag_col_major(vec![1.0_f32, 2.0], 2).unwrap();
1965    /// assert_eq!(diag.payload_f32_col_major_vec().unwrap(), vec![1.0_f32, 2.0]);
1966    /// ```
1967    pub fn payload_f32_col_major_vec(&self) -> StorageResult<Vec<f32>> {
1968        <f32 as StorageScalar>::payload_vec(self)
1969    }
1970
1971    /// Borrows the compact `f32` payload when it is already contiguous in
1972    /// column-major payload order.
1973    ///
1974    /// # Errors
1975    ///
1976    /// Returns `StorageError::ScalarKindMismatch` when the storage is not
1977    /// f32-backed (a dtype mismatch).
1978    pub fn payload_f32_col_major_view_if_contiguous(&self) -> StorageResult<Option<&[f32]>> {
1979        <f32 as StorageScalar>::payload_view(self)
1980    }
1981
1982    /// Copies the compact `f64` payload in column-major payload order.
1983    ///
1984    /// This does not materialize logical dense values. For diagonal storage the
1985    /// returned vector contains only diagonal payload values.
1986    ///
1987    /// # Errors
1988    ///
1989    /// Returns [`StorageError::ScalarKindMismatch`] if the storage scalar type
1990    /// is not `f64`.
1991    ///
1992    /// # Examples
1993    ///
1994    /// ```
1995    /// use tensor4all_tensorbackend::Storage;
1996    ///
1997    /// let diag = Storage::from_diag_col_major(vec![1.0_f64, 2.0], 2).unwrap();
1998    /// assert_eq!(diag.payload_f64_col_major_vec().unwrap(), vec![1.0, 2.0]);
1999    /// ```
2000    pub fn payload_f64_col_major_vec(&self) -> StorageResult<Vec<f64>> {
2001        <f64 as StorageScalar>::payload_vec(self)
2002    }
2003
2004    /// Borrows the compact `f64` payload when it is already contiguous in
2005    /// column-major payload order.
2006    ///
2007    /// # Errors
2008    ///
2009    /// Returns `StorageError::ScalarKindMismatch` when the storage is not
2010    /// f64-backed (a dtype mismatch).
2011    pub fn payload_f64_col_major_view_if_contiguous(&self) -> StorageResult<Option<&[f64]>> {
2012        <f64 as StorageScalar>::payload_view(self)
2013    }
2014
2015    /// Copies the compact `Complex32` payload in column-major payload order.
2016    ///
2017    /// # Errors
2018    ///
2019    /// Returns [`StorageError::ScalarKindMismatch`] if the storage scalar type
2020    /// is not `Complex32`.
2021    ///
2022    /// # Examples
2023    ///
2024    /// ```
2025    /// use num_complex::Complex32;
2026    /// use tensor4all_tensorbackend::Storage;
2027    ///
2028    /// let data = vec![Complex32::new(1.0, -2.0), Complex32::new(0.5, 0.0)];
2029    /// let diag = Storage::from_diag_col_major(data.clone(), 2).unwrap();
2030    /// assert_eq!(diag.payload_c32_col_major_vec().unwrap(), data);
2031    /// ```
2032    pub fn payload_c32_col_major_vec(&self) -> StorageResult<Vec<Complex32>> {
2033        <Complex32 as StorageScalar>::payload_vec(self)
2034    }
2035
2036    /// Borrows the compact `Complex32` payload when it is already contiguous in
2037    /// column-major payload order.
2038    ///
2039    /// # Errors
2040    ///
2041    /// Returns `StorageError::ScalarKindMismatch` when the storage is not
2042    /// c32-backed (a dtype mismatch).
2043    pub fn payload_c32_col_major_view_if_contiguous(&self) -> StorageResult<Option<&[Complex32]>> {
2044        <Complex32 as StorageScalar>::payload_view(self)
2045    }
2046
2047    /// Copies the compact `Complex64` payload in column-major payload order.
2048    ///
2049    /// This does not materialize logical dense values. Complex payloads are
2050    /// returned as native Rust `Complex64` values.
2051    ///
2052    /// # Errors
2053    ///
2054    /// Returns [`StorageError::ScalarKindMismatch`] if the storage scalar type
2055    /// is not `Complex64`.
2056    ///
2057    /// # Examples
2058    ///
2059    /// ```
2060    /// use num_complex::Complex64;
2061    /// use tensor4all_tensorbackend::Storage;
2062    ///
2063    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)];
2064    /// let diag = Storage::from_diag_col_major(data.clone(), 2).unwrap();
2065    /// assert_eq!(diag.payload_c64_col_major_vec().unwrap(), data);
2066    /// ```
2067    pub fn payload_c64_col_major_vec(&self) -> StorageResult<Vec<Complex64>> {
2068        <Complex64 as StorageScalar>::payload_vec(self)
2069    }
2070
2071    /// Borrows the compact `Complex64` payload when it is already contiguous in
2072    /// column-major payload order.
2073    ///
2074    /// # Errors
2075    ///
2076    /// Returns `StorageError::ScalarKindMismatch` when the storage is not
2077    /// c64-backed (a dtype mismatch).
2078    pub fn payload_c64_col_major_view_if_contiguous(&self) -> StorageResult<Option<&[Complex64]>> {
2079        <Complex64 as StorageScalar>::payload_view(self)
2080    }
2081
2082    /// Check if this storage uses f64 scalar type.
2083    ///
2084    /// # Examples
2085    ///
2086    /// ```
2087    /// use tensor4all_tensorbackend::Storage;
2088    ///
2089    /// let s = Storage::from_dense_col_major(vec![1.0_f64], &[1]).unwrap();
2090    /// assert!(s.is_f64());
2091    /// assert!(!s.is_c64());
2092    /// ```
2093    pub fn is_f64(&self) -> bool {
2094        self.dtype() == DType::F64
2095    }
2096
2097    /// Check if this storage uses f32 scalar type.
2098    ///
2099    /// # Examples
2100    ///
2101    /// ```
2102    /// use tensor4all_tensorbackend::Storage;
2103    ///
2104    /// let s = Storage::from_dense_col_major(vec![1.0_f32], &[1]).unwrap();
2105    /// assert!(s.is_f32());
2106    /// assert!(!s.is_f64());
2107    /// ```
2108    pub fn is_f32(&self) -> bool {
2109        self.dtype() == DType::F32
2110    }
2111
2112    /// Check if this storage uses Complex64 scalar type.
2113    ///
2114    /// # Examples
2115    ///
2116    /// ```
2117    /// use tensor4all_tensorbackend::Storage;
2118    /// use num_complex::Complex64;
2119    ///
2120    /// let s = Storage::from_dense_col_major(
2121    ///     vec![Complex64::new(1.0, 0.0)], &[1],
2122    /// ).unwrap();
2123    /// assert!(s.is_c64());
2124    /// ```
2125    pub fn is_c64(&self) -> bool {
2126        self.dtype() == DType::C64
2127    }
2128
2129    /// Check if this storage uses Complex32 scalar type.
2130    ///
2131    /// # Examples
2132    ///
2133    /// ```
2134    /// use num_complex::Complex32;
2135    /// use tensor4all_tensorbackend::Storage;
2136    ///
2137    /// let s = Storage::from_dense_col_major(
2138    ///     vec![Complex32::new(1.0, -1.0)], &[1],
2139    /// ).unwrap();
2140    /// assert!(s.is_c32());
2141    /// assert!(s.is_complex());
2142    /// ```
2143    pub fn is_c32(&self) -> bool {
2144        self.dtype() == DType::C32
2145    }
2146
2147    /// Check if this storage uses complex scalar type.
2148    ///
2149    /// This is `Complex64` or `Complex32`; it is not an alias for
2150    /// [`is_c64()`](Self::is_c64).
2151    ///
2152    /// # Examples
2153    ///
2154    /// ```
2155    /// use num_complex::Complex32;
2156    /// use tensor4all_tensorbackend::Storage;
2157    ///
2158    /// let s = Storage::from_dense_col_major(vec![Complex32::new(1.0, 0.0)], &[1]).unwrap();
2159    /// assert!(s.is_complex());
2160    ///
2161    /// let r = Storage::from_dense_col_major(vec![1.0_f64], &[1]).unwrap();
2162    /// assert!(!r.is_complex());
2163    /// ```
2164    pub fn is_complex(&self) -> bool {
2165        self.0.is_complex()
2166    }
2167
2168    /// Returns the stored scalar dtype.
2169    ///
2170    /// The dtype is metadata only and never depends on payload contents. It is
2171    /// the value a publication or conversion boundary should compare against
2172    /// instead of assuming the payload is `f64` or `Complex64`.
2173    ///
2174    /// # Examples
2175    ///
2176    /// ```
2177    /// use tensor4all_tensorbackend::Storage;
2178    /// use tenferro::DType;
2179    ///
2180    /// let dense = Storage::from_dense_col_major(vec![1.0_f32, 2.0], &[2]).unwrap();
2181    /// assert_eq!(dense.dtype(), DType::F32);
2182    ///
2183    /// let complex = Storage::from_diag_col_major(
2184    ///     vec![num_complex::Complex64::new(1.0, 0.0)], 2,
2185    /// ).unwrap();
2186    /// assert_eq!(complex.dtype(), DType::C64);
2187    /// ```
2188    pub fn dtype(&self) -> DType {
2189        self.0.dtype()
2190    }
2191
2192    /// Returns this storage's scalar-kind label used in diagnostics.
2193    ///
2194    /// # Examples
2195    ///
2196    /// ```
2197    /// use tensor4all_tensorbackend::Storage;
2198    ///
2199    /// let s = Storage::from_dense_col_major(vec![1.0_f32], &[1]).unwrap();
2200    /// assert_eq!(s.scalar_kind(), "f32");
2201    /// ```
2202    pub fn scalar_kind(&self) -> &'static str {
2203        self.0.scalar_kind()
2204    }
2205
2206    /// Get the length of the storage payload (number of stored elements).
2207    ///
2208    /// For dense storage this equals the product of logical dimensions.
2209    /// For diagonal storage this equals the diagonal length.
2210    ///
2211    /// # Examples
2212    ///
2213    /// ```
2214    /// use tensor4all_tensorbackend::Storage;
2215    ///
2216    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0], &[3]).unwrap();
2217    /// assert_eq!(s.len(), 3);
2218    ///
2219    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
2220    /// assert_eq!(d.len(), 2);
2221    /// ```
2222    pub fn len(&self) -> usize {
2223        storage_payload_dispatch!(&self.0, Payload => |value| value.len())
2224    }
2225
2226    /// Check if the storage is empty.
2227    ///
2228    /// # Examples
2229    ///
2230    /// ```
2231    /// use tensor4all_tensorbackend::Storage;
2232    ///
2233    /// let s = Storage::new_dense::<f64>(0).unwrap();
2234    /// assert!(s.is_empty());
2235    ///
2236    /// let s2 = Storage::new_dense::<f64>(3).unwrap();
2237    /// assert!(!s2.is_empty());
2238    /// ```
2239    pub fn is_empty(&self) -> bool {
2240        self.len() == 0
2241    }
2242
2243    /// Sum all referenced compact payload entries, converting to type `T`.
2244    ///
2245    /// Stride gaps and padding in a structured backing buffer are not tensor
2246    /// values and are excluded from the reduction.
2247    ///
2248    /// # Example
2249    /// ```
2250    /// use tensor4all_tensorbackend::Storage;
2251    /// let s = Storage::from_dense_col_major(vec![1.0, 2.0, 3.0], &[3]).unwrap();
2252    /// assert_eq!(s.sum::<f64>(), 6.0);
2253    /// ```
2254    pub fn sum<T: SumFromStorage>(&self) -> T {
2255        T::sum_from_storage(self)
2256    }
2257
2258    /// Maximum absolute value over all referenced compact payload entries.
2259    ///
2260    /// Stride gaps and padding are excluded. For real storage this is `max(|x|)`, and for complex storage this is
2261    /// `max(hypot(re, im))`. NaN payloads, including a NaN real or imaginary
2262    /// component paired with infinity, propagate to a NaN result; positive
2263    /// infinity is preserved.
2264    ///
2265    /// # Examples
2266    ///
2267    /// ```
2268    /// use tensor4all_tensorbackend::Storage;
2269    ///
2270    /// let s = Storage::from_dense_col_major(vec![-3.0_f64, 1.0, 2.0], &[3]).unwrap();
2271    /// assert!((s.max_abs() - 3.0).abs() < 1e-10);
2272    /// ```
2273    pub fn max_abs(&self) -> f64 {
2274        fn fold_nan_propagating(current: f64, value: f64) -> f64 {
2275            if current.is_nan() || value.is_nan() {
2276                f64::NAN
2277            } else {
2278                current.max(value)
2279            }
2280        }
2281
2282        let mut current = 0.0_f64;
2283        self.for_each_payload_magnitude(|magnitude| {
2284            current = fold_nan_propagating(current, magnitude);
2285        });
2286        current
2287    }
2288
2289    /// Scan the compact payload without copying it.
2290    ///
2291    /// The returned flags describe the stored payload, not the logical dense
2292    /// tensor. Structural zeros cannot introduce non-finite values, so this is
2293    /// sufficient for metric validation while preserving compact memory use.
2294    ///
2295    /// # Examples
2296    ///
2297    /// ```
2298    /// use tensor4all_tensorbackend::Storage;
2299    ///
2300    /// let storage = Storage::from_dense_col_major(vec![1.0_f64, f64::INFINITY], &[2]).unwrap();
2301    /// let flags = storage.payload_nonfinite_flags();
2302    /// assert!(!flags.has_nan);
2303    /// assert!(flags.has_infinity);
2304    /// ```
2305    pub fn payload_nonfinite_flags(&self) -> NonfiniteFlags {
2306        let mut flags = NonfiniteFlags::default();
2307        storage_payload_dispatch!(&self.0, Payload => |payload| {
2308            payload.for_each_payload_value(|value| {
2309                let (re, im) = <Payload as StorageScalar>::widened_parts(value);
2310                flags.has_nan |= re.is_nan() || im.is_nan();
2311                flags.has_infinity |= re.is_infinite() || im.is_infinite();
2312            })
2313        });
2314        flags
2315    }
2316
2317    /// Return one compact payload value as a dynamic [`BackendScalar`].
2318    ///
2319    /// `payload_coords` are column-major payload coordinates, not logical
2320    /// coordinates. Repeated logical axis classes are represented once in the
2321    /// payload, so this lookup performs only rank-bounded stride arithmetic and
2322    /// never allocates coordinates or scans the payload.
2323    ///
2324    /// # Errors
2325    ///
2326    /// Returns [`StorageError`] when the coordinate rank or bounds are invalid
2327    /// or the compact metadata points outside the backing payload.
2328    ///
2329    /// # Examples
2330    ///
2331    /// ```
2332    /// use num_complex::Complex64;
2333    /// use tensor4all_tensorbackend::Storage;
2334    ///
2335    /// let real = Storage::from_diag_col_major(vec![2.0_f64, 3.0], 2).unwrap();
2336    /// assert_eq!(real.scalar_at(&[1]).unwrap().real(), 3.0);
2337    ///
2338    /// let complex = Storage::from_dense_col_major(
2339    ///     vec![Complex64::new(1.0, -2.0)], &[1],
2340    /// ).unwrap();
2341    /// assert_eq!(complex.scalar_at(&[0]).unwrap().as_c64(),
2342    ///     Some(Complex64::new(1.0, -2.0)));
2343    /// ```
2344    pub fn scalar_at(&self, payload_coords: &[usize]) -> StorageResult<BackendScalar> {
2345        storage_payload_dispatch!(&self.0, Payload => |payload| {
2346            payload
2347                .payload_scalar_at(payload_coords)
2348                .map(<Payload as StorageScalar>::to_backend_scalar)
2349        })
2350    }
2351
2352    /// Materialize dense logical values as a column-major `f64` buffer.
2353    ///
2354    /// For diagonal storage, off-diagonal entries are filled with zero.
2355    ///
2356    /// # Errors
2357    ///
2358    /// Returns [`StorageError::ScalarKindMismatch`] if the storage is complex,
2359    /// and [`StorageError::InvalidStructuredStorage`] if `logical_dims` does
2360    /// not match the stored logical dimensions or the logical element count
2361    /// overflows `usize`.
2362    ///
2363    /// # Examples
2364    ///
2365    /// ```
2366    /// use tensor4all_tensorbackend::Storage;
2367    ///
2368    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0, 4.0], &[2, 2]).unwrap();
2369    /// let dense = s.to_dense_f64_col_major_vec(&[2, 2]).unwrap();
2370    /// assert_eq!(dense, vec![1.0, 2.0, 3.0, 4.0]);
2371    /// ```
2372    pub fn to_dense_f64_col_major_vec(&self, logical_dims: &[usize]) -> StorageResult<Vec<f64>> {
2373        <f64 as StorageScalar>::dense_values(self, logical_dims)
2374    }
2375
2376    /// Materialize dense logical values as a column-major `f32` buffer.
2377    ///
2378    /// For diagonal storage, off-diagonal entries are filled with zero.
2379    ///
2380    /// # Errors
2381    ///
2382    /// Returns [`StorageError::ScalarKindMismatch`] unless the storage is
2383    /// `f32`-backed, and [`StorageError::InvalidStructuredStorage`] if
2384    /// `logical_dims` does not match the stored logical dimensions or the
2385    /// logical element count overflows `usize`.
2386    ///
2387    /// # Examples
2388    ///
2389    /// ```
2390    /// use tensor4all_tensorbackend::Storage;
2391    ///
2392    /// let diag = Storage::new_diag(vec![1.0_f32, 2.0]).unwrap();
2393    /// assert_eq!(diag.to_dense_f32_col_major_vec(&[2, 2]).unwrap(), vec![1.0, 0.0, 0.0, 2.0]);
2394    /// ```
2395    pub fn to_dense_f32_col_major_vec(&self, logical_dims: &[usize]) -> StorageResult<Vec<f32>> {
2396        <f32 as StorageScalar>::dense_values(self, logical_dims)
2397    }
2398
2399    /// Materialize dense logical values as a column-major `Complex64` buffer.
2400    ///
2401    /// # Errors
2402    ///
2403    /// Returns [`StorageError::ScalarKindMismatch`] unless the storage is
2404    /// `Complex64`-backed, and [`StorageError::InvalidStructuredStorage`] if
2405    /// `logical_dims` does not match the stored logical dimensions or the
2406    /// logical element count overflows `usize`.
2407    ///
2408    /// # Examples
2409    ///
2410    /// ```
2411    /// use tensor4all_tensorbackend::Storage;
2412    /// use num_complex::Complex64;
2413    ///
2414    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)];
2415    /// let s = Storage::from_dense_col_major(data.clone(), &[2]).unwrap();
2416    /// let dense = s.to_dense_c64_col_major_vec(&[2]).unwrap();
2417    /// assert_eq!(dense, data);
2418    /// ```
2419    pub fn to_dense_c64_col_major_vec(
2420        &self,
2421        logical_dims: &[usize],
2422    ) -> StorageResult<Vec<Complex64>> {
2423        <Complex64 as StorageScalar>::dense_values(self, logical_dims)
2424    }
2425
2426    /// Materialize dense logical values as a column-major `Complex32` buffer.
2427    ///
2428    /// # Errors
2429    ///
2430    /// Returns [`StorageError::ScalarKindMismatch`] unless the storage is
2431    /// `Complex32`-backed, and [`StorageError::InvalidStructuredStorage`] if
2432    /// `logical_dims` does not match the stored logical dimensions or the
2433    /// logical element count overflows `usize`.
2434    ///
2435    /// # Examples
2436    ///
2437    /// ```
2438    /// use num_complex::Complex32;
2439    /// use tensor4all_tensorbackend::Storage;
2440    ///
2441    /// let data = vec![Complex32::new(1.0, -1.0), Complex32::new(2.0, 0.5)];
2442    /// let s = Storage::from_dense_col_major(data.clone(), &[2]).unwrap();
2443    /// assert_eq!(s.to_dense_c32_col_major_vec(&[2]).unwrap(), data);
2444    /// ```
2445    pub fn to_dense_c32_col_major_vec(
2446        &self,
2447        logical_dims: &[usize],
2448    ) -> StorageResult<Vec<Complex32>> {
2449        <Complex32 as StorageScalar>::dense_values(self, logical_dims)
2450    }
2451
2452    /// Convert this storage to dense storage.
2453    ///
2454    /// For Diag storage, creates a Dense storage with diagonal elements set
2455    /// and off-diagonal elements as zero. For Dense storage, returns a copy.
2456    ///
2457    /// # Errors
2458    ///
2459    /// Returns [`StorageError::InvalidStructuredStorage`] if `dims` does not
2460    /// match the stored logical dimensions or the dense storage cannot be built.
2461    ///
2462    /// # Examples
2463    ///
2464    /// ```
2465    /// use tensor4all_tensorbackend::Storage;
2466    ///
2467    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
2468    /// let dense = d.to_dense_storage(&[2, 2]).unwrap();
2469    /// assert!(dense.is_dense());
2470    /// let vals = dense.to_dense_f64_col_major_vec(&[2, 2]).unwrap();
2471    /// assert_eq!(vals, vec![1.0, 0.0, 0.0, 2.0]);
2472    /// ```
2473    pub fn to_dense_storage(&self, dims: &[usize]) -> StorageResult<Storage> {
2474        storage_payload_dispatch!(&self.0, Payload => |payload| {
2475            let values = checked_dense_payload(payload, dims)?;
2476            Storage::from_dense_col_major(values, dims).map_err(Self::invalid_storage_error)
2477        })
2478    }
2479
2480    /// Permute the storage data according to the given permutation.
2481    ///
2482    /// The `_dims` parameter is currently unused (reserved for future use).
2483    ///
2484    /// # Examples
2485    ///
2486    /// ```
2487    /// use tensor4all_tensorbackend::Storage;
2488    ///
2489    /// // Diagonal 2x2 tensor, permute axes (identity perm for diag is valid)
2490    /// let d = Storage::new_diag(vec![1.0_f64, 2.0]).unwrap();
2491    /// let t = d.permute_storage(&[2, 2], &[1, 0]).unwrap();
2492    /// assert!(t.is_diag());
2493    /// ```
2494    ///
2495    /// # Errors
2496    ///
2497    /// Returns [`StorageError::Operation`] when `perm` is not a permutation of
2498    /// the logical axes.
2499    pub fn permute_storage(&self, _dims: &[usize], perm: &[usize]) -> StorageResult<Storage> {
2500        storage_payload_dispatch!(&self.0, Payload => |payload| {
2501            Ok(payload.permute_logical_axes(perm)?.into_storage())
2502        })
2503    }
2504
2505    /// Extract real part from Complex64 storage as f64 storage.
2506    /// For f64 storage, returns a copy (clone).
2507    ///
2508    /// # Examples
2509    ///
2510    /// ```
2511    /// use tensor4all_tensorbackend::Storage;
2512    /// use num_complex::Complex64;
2513    ///
2514    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)];
2515    /// let s = Storage::from_dense_col_major(data, &[2]).unwrap();
2516    /// let re = s.extract_real_part();
2517    /// assert!(re.is_f64());
2518    /// assert_eq!(re.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![1.0, 3.0]);
2519    /// ```
2520    pub fn extract_real_part(&self) -> Storage {
2521        storage_payload_dispatch!(&self.0, Payload => |payload| {
2522            payload
2523                .map_copy(<Payload as StorageScalar>::real_part)
2524                .into_storage()
2525        })
2526    }
2527
2528    /// Extract imaginary part from complex storage as the matching real storage.
2529    ///
2530    /// `Complex64` yields `f64` storage and `Complex32` yields `f32` storage.
2531    /// For real storage, returns zero storage of the same real dtype.
2532    ///
2533    /// # Examples
2534    ///
2535    /// ```
2536    /// use tensor4all_tensorbackend::Storage;
2537    /// use num_complex::Complex64;
2538    ///
2539    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)];
2540    /// let s = Storage::from_dense_col_major(data, &[2]).unwrap();
2541    /// let im = s.extract_imag_part(&[2]);
2542    /// assert!(im.is_f64());
2543    /// assert_eq!(im.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![2.0, 4.0]);
2544    /// ```
2545    pub fn extract_imag_part(&self, _dims: &[usize]) -> Storage {
2546        storage_payload_dispatch!(&self.0, Payload => |payload| {
2547            payload
2548                .map_copy(<Payload as StorageScalar>::imag_part)
2549                .into_storage()
2550        })
2551    }
2552
2553    /// Convert real storage to its complex counterpart (zero imaginary part).
2554    ///
2555    /// `f64` becomes `Complex64` and `f32` becomes `Complex32`; complex storage
2556    /// returns a clone of the same dtype.
2557    ///
2558    /// # Examples
2559    ///
2560    /// ```
2561    /// use tensor4all_tensorbackend::Storage;
2562    ///
2563    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
2564    /// let c = s.to_complex_storage();
2565    /// assert!(c.is_c64());
2566    ///
2567    /// let narrow = Storage::from_dense_col_major(vec![1.0_f32], &[1]).unwrap();
2568    /// assert!(narrow.to_complex_storage().is_c32());
2569    /// ```
2570    pub fn to_complex_storage(&self) -> Storage {
2571        storage_payload_dispatch!(&self.0, Payload => |payload| {
2572            payload
2573                .map_copy(<Payload as StorageScalar>::into_complex)
2574                .into_storage()
2575        })
2576    }
2577
2578    /// Complex conjugate of all elements.
2579    ///
2580    /// For real storage, returns a clone (conjugate of a real number is itself).
2581    /// For complex storage, conjugates each element without changing the dtype.
2582    ///
2583    /// This is inspired by the `conj` operation in ITensorMPS.jl.
2584    ///
2585    /// # Examples
2586    /// ```
2587    /// use tensor4all_tensorbackend::Storage;
2588    /// use num_complex::Complex64;
2589    ///
2590    /// let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, -4.0)];
2591    /// let storage = Storage::from_dense_col_major(data, &[2]).unwrap();
2592    /// let conj_storage = storage.conj();
2593    ///
2594    /// let result = conj_storage.to_dense_c64_col_major_vec(&[2]).unwrap();
2595    /// assert_eq!(result[0], Complex64::new(1.0, -2.0));
2596    /// assert_eq!(result[1], Complex64::new(3.0, 4.0));
2597    /// ```
2598    pub fn conj(&self) -> Self {
2599        storage_payload_dispatch!(&self.0, Payload => |payload| {
2600            payload
2601                .map_copy(<Payload as StorageScalar>::conjugate)
2602                .into_storage()
2603        })
2604    }
2605
2606    /// Combine two real storages into their complex counterpart.
2607    ///
2608    /// `real_storage` becomes the real part and `imag_storage` the imaginary
2609    /// part, so `f64 + f64` yields `Complex64` storage and `f32 + f32` yields
2610    /// `Complex32` storage. Nothing is widened to a wider dtype.
2611    ///
2612    /// # Errors
2613    ///
2614    /// Returns [`StorageError::LengthMismatch`] if the payload lengths differ,
2615    /// [`StorageError::OperationNotSupported`] if the two storages do not share
2616    /// one real scalar kind, and
2617    /// [`StorageError::InvalidStructuredStorage`] if the result layout cannot
2618    /// be built.
2619    ///
2620    /// # Examples
2621    ///
2622    /// ```
2623    /// use tensor4all_tensorbackend::Storage;
2624    /// use num_complex::Complex64;
2625    ///
2626    /// let re = Storage::from_dense_col_major(vec![1.0_f64, 3.0], &[2]).unwrap();
2627    /// let im = Storage::from_dense_col_major(vec![2.0_f64, 4.0], &[2]).unwrap();
2628    /// let c = Storage::combine_to_complex(&re, &im).unwrap();
2629    /// assert!(c.is_c64());
2630    /// let vals = c.to_dense_c64_col_major_vec(&[2]).unwrap();
2631    /// assert_eq!(vals[0], Complex64::new(1.0, 2.0));
2632    /// assert_eq!(vals[1], Complex64::new(3.0, 4.0));
2633    ///
2634    /// let narrow_re = Storage::from_dense_col_major(vec![1.0_f32], &[1]).unwrap();
2635    /// let narrow_im = Storage::from_dense_col_major(vec![2.0_f32], &[1]).unwrap();
2636    /// assert!(Storage::combine_to_complex(&narrow_re, &narrow_im)
2637    ///     .unwrap()
2638    ///     .is_c32());
2639    /// ```
2640    pub fn combine_to_complex(
2641        real_storage: &Storage,
2642        imag_storage: &Storage,
2643    ) -> StorageResult<Storage> {
2644        match (&real_storage.0, &imag_storage.0) {
2645            (StorageRepr::F32(real), StorageRepr::F32(imag)) => {
2646                zip_payloads(real, imag, "combine_to_complex", |re, im| {
2647                    Complex32::new(re, im)
2648                })
2649            }
2650            (StorageRepr::F64(real), StorageRepr::F64(imag)) => {
2651                zip_payloads(real, imag, "combine_to_complex", |re, im| {
2652                    Complex64::new(re, im)
2653                })
2654            }
2655            _ => Err(StorageError::OperationNotSupported {
2656                operation: "combine_to_complex",
2657                left: real_storage.scalar_kind(),
2658                right: imag_storage.scalar_kind(),
2659            }),
2660        }
2661    }
2662
2663    /// Add two storages element-wise, returning `Result` on error instead of panicking.
2664    ///
2665    /// Both storages must have the same scalar type and payload length; mixed
2666    /// scalar kinds are refused rather than promoted.
2667    ///
2668    /// # Errors
2669    ///
2670    /// Returns [`StorageError::LengthMismatch`] if the lengths differ,
2671    /// [`StorageError::OperationNotSupported`] if the scalar types differ, and
2672    /// [`StorageError::InvalidStructuredStorage`] if the result layout cannot be
2673    /// built.
2674    ///
2675    /// # Examples
2676    ///
2677    /// ```
2678    /// use tensor4all_tensorbackend::Storage;
2679    ///
2680    /// let a = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
2681    /// let b = Storage::from_dense_col_major(vec![3.0_f64, 4.0], &[2]).unwrap();
2682    /// let c = a.try_add(&b).unwrap();
2683    /// assert_eq!(c.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![4.0, 6.0]);
2684    ///
2685    /// let narrow = Storage::from_dense_col_major(vec![0.5_f32, 0.5], &[2]).unwrap();
2686    /// let sum = narrow.try_add(&narrow).unwrap();
2687    /// assert!(sum.is_f32());
2688    /// assert_eq!(sum.to_dense_f32_col_major_vec(&[2]).unwrap(), vec![1.0, 1.0]);
2689    /// ```
2690    pub fn try_add(&self, other: &Storage) -> StorageResult<Storage> {
2691        storage_payload_same_dtype_dispatch!(&self.0, &other.0, Payload => |left, right| {
2692            zip_payloads(left, right, "addition", |x, y| x + y)
2693        },
2694        else Err(StorageError::OperationNotSupported {
2695            operation: "addition",
2696            left: self.scalar_kind(),
2697            right: other.scalar_kind(),
2698        }))
2699    }
2700
2701    /// Try to subtract two storages element-wise.
2702    ///
2703    /// Both storages must have the same scalar type and payload length; mixed
2704    /// scalar kinds are refused rather than promoted.
2705    ///
2706    /// # Errors
2707    ///
2708    /// Returns [`StorageError::LengthMismatch`] if the lengths differ,
2709    /// [`StorageError::OperationNotSupported`] if the scalar types differ, and
2710    /// [`StorageError::InvalidStructuredStorage`] if the result layout cannot be
2711    /// built.
2712    ///
2713    /// # Examples
2714    ///
2715    /// ```
2716    /// use tensor4all_tensorbackend::Storage;
2717    ///
2718    /// let a = Storage::from_dense_col_major(vec![5.0_f64, 7.0], &[2]).unwrap();
2719    /// let b = Storage::from_dense_col_major(vec![1.0_f64, 3.0], &[2]).unwrap();
2720    /// let c = a.try_sub(&b).unwrap();
2721    /// assert_eq!(c.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![4.0, 4.0]);
2722    /// ```
2723    pub fn try_sub(&self, other: &Storage) -> StorageResult<Storage> {
2724        storage_payload_same_dtype_dispatch!(&self.0, &other.0, Payload => |left, right| {
2725            zip_payloads(left, right, "subtraction", |x, y| x - y)
2726        },
2727        else Err(StorageError::OperationNotSupported {
2728            operation: "subtraction",
2729            left: self.scalar_kind(),
2730            right: other.scalar_kind(),
2731        }))
2732    }
2733
2734    /// Scale storage by a dynamic scalar value.
2735    ///
2736    /// The payload keeps its width and gains a complex component when the
2737    /// scalar is complex (`f64` -> `Complex64`, `f32` -> `Complex32`), which is
2738    /// exactly [`scale_target_dtype`]. Real scalars never widen a narrow
2739    /// payload.
2740    ///
2741    /// # Examples
2742    ///
2743    /// ```
2744    /// use tensor4all_tensorbackend::{BackendScalar, Storage};
2745    ///
2746    /// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0, 3.0], &[3]).unwrap();
2747    /// let scaled = s.scale(&BackendScalar::new_real(2.0));
2748    /// assert_eq!(scaled.to_dense_f64_col_major_vec(&[3]).unwrap(), vec![2.0, 4.0, 6.0]);
2749    ///
2750    /// let narrow = Storage::from_dense_col_major(vec![1.0_f32, 2.0], &[2]).unwrap();
2751    /// let complex = narrow.scale(&BackendScalar::new_complex(0.0, 1.0));
2752    /// assert!(complex.is_c32());
2753    /// ```
2754    pub fn scale(&self, scalar: &crate::BackendScalar) -> Storage {
2755        self * scalar.clone()
2756    }
2757
2758    /// Compute linear combination: `a * self + b * other`.
2759    ///
2760    /// Promotion has one rule, shared with [`Storage::scale`]: the two payload
2761    /// dtypes promote along the lattice `f32 < f64`, `Complex32 < Complex64`,
2762    /// real `<` complex, while a scalar never widens the payload dtype - it only
2763    /// adds a complex component at the promoted width. So `f32 + f32` with
2764    /// `f64` scalars stays `f32`, `f32 + f64` becomes `f64`, and a complex
2765    /// scalar turns an `f32` result into `Complex32`.
2766    ///
2767    /// # Errors
2768    ///
2769    /// Returns [`StorageError::LengthMismatch`] if the lengths differ,
2770    /// [`StorageError::OperationNotSupported`] when an operand dtype is not a
2771    /// supported floating or complex floating dtype, and
2772    /// [`StorageError::InvalidStructuredStorage`] if the result layout cannot be
2773    /// built.
2774    ///
2775    /// # Examples
2776    ///
2777    /// ```
2778    /// use tensor4all_tensorbackend::{BackendScalar, Storage};
2779    ///
2780    /// let x = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
2781    /// let y = Storage::from_dense_col_major(vec![3.0_f64, 4.0], &[2]).unwrap();
2782    /// let a = BackendScalar::new_real(2.0);
2783    /// let b = BackendScalar::new_real(3.0);
2784    /// // result = 2*[1,2] + 3*[3,4] = [11, 16]
2785    /// let result = x.axpby(&a, &y, &b).unwrap();
2786    /// assert_eq!(result.to_dense_f64_col_major_vec(&[2]).unwrap(), vec![11.0, 16.0]);
2787    ///
2788    /// // Real scalars do not widen a narrow payload.
2789    /// let narrow = Storage::from_dense_col_major(vec![1.0_f32], &[1]).unwrap();
2790    /// assert!(narrow.axpby(&a, &narrow, &b).unwrap().is_f32());
2791    ///
2792    /// // A complex scalar adds complexity at the payload width.
2793    /// use num_complex::Complex32;
2794    /// let mixed = narrow
2795    ///     .axpby(
2796    ///         &BackendScalar::from(Complex32::new(0.0, 1.0)),
2797    ///         &narrow,
2798    ///         &BackendScalar::from(1.0_f32),
2799    ///     )
2800    ///     .unwrap();
2801    /// assert!(mixed.is_c32());
2802    /// assert_eq!(
2803    ///     mixed.to_dense_c32_col_major_vec(&[1]).unwrap(),
2804    ///     vec![Complex32::new(1.0, 1.0)],
2805    /// );
2806    /// ```
2807    pub fn axpby(
2808        &self,
2809        a: &crate::BackendScalar,
2810        other: &Storage,
2811        b: &crate::BackendScalar,
2812    ) -> StorageResult<Storage> {
2813        if self.len() != other.len() {
2814            return Err(StorageError::LengthMismatch {
2815                operation: "axpby",
2816                left: self.len(),
2817                right: other.len(),
2818            });
2819        }
2820
2821        // One promotion rule with `scale`: the payloads promote along the lattice, then the scalars may
2822        // add a complex component at that width but never widen it.
2823        let payload_dtype =
2824            promote_float_dtypes([self.dtype(), other.dtype()]).ok_or_else(|| {
2825                StorageError::OperationNotSupported {
2826                    operation: "axpby",
2827                    left: self.scalar_kind(),
2828                    right: other.scalar_kind(),
2829                }
2830            })?;
2831        let target = scale_target_dtype(payload_dtype, scalar_dtype(a))
2832            .and_then(|dtype| scale_target_dtype(dtype, scalar_dtype(b)))
2833            .map_err(|_| StorageError::OperationNotSupported {
2834                operation: "axpby",
2835                left: self.scalar_kind(),
2836                right: other.scalar_kind(),
2837            })?;
2838        let (a_re, a_im) = (a.real(), a.imag());
2839        let (b_re, b_im) = (b.real(), b.imag());
2840
2841        storage_dtype_dispatch!(target, Result => {
2842            let values: Vec<Result> =
2843                storage_payload_pair_dispatch!(&self.0, &other.0, Left, Right => |left, right| {
2844                    left.data()
2845                        .iter()
2846                        .zip(right.data().iter())
2847                        .map(|(x, y)| {
2848                            let (x_re, x_im) = <Left as StorageScalar>::widened_parts(*x);
2849                            let (y_re, y_im) = <Right as StorageScalar>::widened_parts(*y);
2850                            <Result as StorageScalar>::from_widened_parts(
2851                                a_re * x_re - a_im * x_im + b_re * y_re - b_im * y_im,
2852                                a_re * x_im + a_im * x_re + b_re * y_im + b_im * y_re,
2853                            )
2854                        })
2855                        .collect()
2856                });
2857            StructuredStorage::new(
2858                values,
2859                self.payload_dims().to_vec(),
2860                self.payload_strides().to_vec(),
2861                self.axis_classes().to_vec(),
2862            )
2863            .map(StructuredStorage::into_storage)
2864            .map_err(StorageError::from)
2865        }, else Err(StorageError::OperationNotSupported {
2866            operation: "axpby",
2867            left: self.scalar_kind(),
2868            right: other.scalar_kind(),
2869        }))
2870    }
2871}
2872
2873/// Result dtype of scaling a compact payload by a dynamic scalar.
2874///
2875/// The payload keeps its width and gains a complex component only when the
2876/// scalar is complex: `f64` scaled by a `Complex32` or `Complex64` scalar stays
2877/// `Complex64`, and `f32` scaled by a complex scalar stays `Complex32`. This is
2878/// the compact-storage counterpart of the eager scaling rule in
2879/// `tensor4all-core`, which delegates here so both surfaces agree.
2880///
2881/// # Errors
2882///
2883/// Returns [`StorageError::OperationNotSupported`] when either dtype is not a
2884/// supported floating or complex floating dtype.
2885///
2886/// # Examples
2887///
2888/// ```
2889/// use tensor4all_tensorbackend::scale_target_dtype;
2890/// use tenferro::DType;
2891///
2892/// assert_eq!(scale_target_dtype(DType::F64, DType::C32).unwrap(), DType::C64);
2893/// assert_eq!(scale_target_dtype(DType::F32, DType::C64).unwrap(), DType::C32);
2894/// assert_eq!(scale_target_dtype(DType::C32, DType::F64).unwrap(), DType::C32);
2895/// assert_eq!(scale_target_dtype(DType::F64, DType::F64).unwrap(), DType::F64);
2896/// assert!(scale_target_dtype(DType::I32, DType::F64).is_err());
2897/// ```
2898pub fn scale_target_dtype(payload: DType, scalar: DType) -> StorageResult<DType> {
2899    let width =
2900        float_lattice_width(payload).ok_or_else(|| StorageError::OperationNotSupported {
2901            operation: "scaling",
2902            left: dtype_label(payload),
2903            right: dtype_label(scalar),
2904        })?;
2905    if float_lattice_width(scalar).is_none() {
2906        return Err(StorageError::OperationNotSupported {
2907            operation: "scaling",
2908            left: dtype_label(payload),
2909            right: dtype_label(scalar),
2910        });
2911    }
2912    let complex =
2913        matches!(payload, DType::C32 | DType::C64) || matches!(scalar, DType::C32 | DType::C64);
2914    Ok(match (width, complex) {
2915        (32, false) => DType::F32,
2916        (64, false) => DType::F64,
2917        (32, true) => DType::C32,
2918        _ => DType::C64,
2919    })
2920}
2921
2922fn dtype_label(dtype: DType) -> &'static str {
2923    match dtype {
2924        DType::F32 => "f32",
2925        DType::F64 => "f64",
2926        DType::I32 => "i32",
2927        DType::I64 => "i64",
2928        DType::Bool => "bool",
2929        DType::C32 => "Complex32",
2930        DType::C64 => "Complex64",
2931        DType::External(_) => "external",
2932    }
2933}
2934
2935fn scalar_dtype(scalar: &BackendScalar) -> DType {
2936    scalar.as_native().dtype()
2937}
2938
2939/// Helper to get a mutable reference to storage, cloning if needed (COW).
2940///
2941/// Uses `Arc::make_mut` semantics: if the `Arc` has only one strong reference,
2942/// returns a mutable reference to the existing allocation. Otherwise clones
2943/// the inner value first.
2944///
2945/// # Examples
2946///
2947/// ```
2948/// use std::sync::Arc;
2949/// use tensor4all_tensorbackend::{make_mut_storage, Storage};
2950///
2951/// let s = Storage::from_dense_col_major(vec![1.0_f64, 2.0], &[2]).unwrap();
2952/// let mut arc = Arc::new(s);
2953/// let s_mut = make_mut_storage(&mut arc);
2954/// // s_mut is now a mutable reference to Storage
2955/// assert!(s_mut.is_f64());
2956/// ```
2957pub fn make_mut_storage(arc: &mut Arc<Storage>) -> &mut Storage {
2958    Arc::make_mut(arc)
2959}
2960
2961/// Get the minimum dimension from a slice of dimensions.
2962///
2963/// Returns 1 for an empty slice. This is used for DiagTensor where all
2964/// indices must have the same dimension.
2965///
2966/// # Examples
2967///
2968/// ```
2969/// use tensor4all_tensorbackend::min_dim;
2970///
2971/// assert_eq!(min_dim(&[2, 3, 4]), 2);
2972/// assert_eq!(min_dim(&[5, 5, 5]), 5);
2973/// assert_eq!(min_dim(&[]), 1);
2974/// ```
2975pub fn min_dim(dims: &[usize]) -> usize {
2976    dims.iter().copied().min().unwrap_or(1)
2977}
2978
2979/// Contract two storage tensors along specified axes.
2980///
2981/// All storage is StructuredStorage; contraction is delegated to the native
2982/// tenferro backend. This is the primary tensor contraction entry point at
2983/// the storage layer.
2984///
2985/// # Arguments
2986///
2987/// * `storage_a` - First tensor storage
2988/// * `dims_a` - Dimensions of the first tensor
2989/// * `axes_a` - Axes of the first tensor to contract
2990/// * `storage_b` - Second tensor storage
2991/// * `dims_b` - Dimensions of the second tensor
2992/// * `axes_b` - Axes of the second tensor to contract
2993/// * `result_dims` - Dimensions of the result tensor (empty for scalar result)
2994///
2995/// # Returns
2996/// A new `Storage` containing the contracted result.
2997///
2998/// # Errors
2999///
3000/// Returns [`StorageError::InvalidStructuredStorage`] if the axis lists differ
3001/// in length, an axis is out of range, contracted dimensions do not match, or
3002/// the native backend rejects the contraction.
3003///
3004/// # Examples
3005///
3006/// ```
3007/// use tensor4all_tensorbackend::{contract_storage, Storage};
3008///
3009/// // Matrix-vector multiply: A(2x3) * v(3) -> result(2)
3010/// let a = Storage::from_dense_col_major(
3011///     vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3],
3012/// ).unwrap();
3013/// let v = Storage::from_dense_col_major(vec![1.0, 1.0, 1.0], &[3]).unwrap();
3014/// let result = contract_storage(&a, &[2, 3], &[1], &v, &[3], &[0], &[2]).unwrap();
3015/// // Row sums: [1+3+5, 2+4+6] = [9, 12]
3016/// let vals = result.to_dense_f64_col_major_vec(&[2]).unwrap();
3017/// assert!((vals[0] - 9.0).abs() < 1e-10);
3018/// assert!((vals[1] - 12.0).abs() < 1e-10);
3019/// ```
3020pub fn contract_storage(
3021    storage_a: &Storage,
3022    dims_a: &[usize],
3023    axes_a: &[usize],
3024    storage_b: &Storage,
3025    dims_b: &[usize],
3026    axes_b: &[usize],
3027    result_dims: &[usize],
3028) -> StorageResult<Storage> {
3029    try_contract_storage(
3030        storage_a,
3031        dims_a,
3032        axes_a,
3033        storage_b,
3034        dims_b,
3035        axes_b,
3036        result_dims,
3037    )
3038}
3039
3040fn try_contract_storage(
3041    storage_a: &Storage,
3042    dims_a: &[usize],
3043    axes_a: &[usize],
3044    storage_b: &Storage,
3045    dims_b: &[usize],
3046    axes_b: &[usize],
3047    result_dims: &[usize],
3048) -> StorageResult<Storage> {
3049    if axes_a.len() != axes_b.len() {
3050        return Err(StorageError::InvalidStructuredStorage(format!(
3051            "contract axes lengths must match: {} != {}",
3052            axes_a.len(),
3053            axes_b.len()
3054        )));
3055    }
3056
3057    for (&a_axis, &b_axis) in axes_a.iter().zip(axes_b.iter()) {
3058        let Some(&a_dim) = dims_a.get(a_axis) else {
3059            return Err(StorageError::InvalidStructuredStorage(format!(
3060                "contract axis {a_axis} is out of range for left dims {dims_a:?}"
3061            )));
3062        };
3063        let Some(&b_dim) = dims_b.get(b_axis) else {
3064            return Err(StorageError::InvalidStructuredStorage(format!(
3065                "contract axis {b_axis} is out of range for right dims {dims_b:?}"
3066            )));
3067        };
3068        if a_dim != b_dim {
3069            return Err(StorageError::InvalidStructuredStorage(format!(
3070                "contracted dimensions must match: dims_a[{a_axis}] = {a_dim} != dims_b[{b_axis}] = {b_dim}"
3071            )));
3072        }
3073    }
3074
3075    crate::tenferro_bridge::contract_storage_native(
3076        storage_a,
3077        dims_a,
3078        axes_a,
3079        storage_b,
3080        dims_b,
3081        axes_b,
3082        result_dims,
3083    )
3084    .map_err(|err| StorageError::InvalidStructuredStorage(err.to_string()))
3085}
3086
3087/// Multiply storage by a real `f64` scalar.
3088///
3089/// The payload keeps its dtype: a narrow payload is not widened, and complex
3090/// payloads scale both components.
3091impl Mul<f64> for &Storage {
3092    type Output = Storage;
3093
3094    fn mul(self, scalar: f64) -> Self::Output {
3095        storage_payload_dispatch!(&self.0, Payload => |payload| {
3096            payload
3097                .map_copy(|value| {
3098                    let (re, im) = <Payload as StorageScalar>::widened_parts(value);
3099                    <Payload as StorageScalar>::from_widened_parts(re * scalar, im * scalar)
3100                })
3101                .into_storage()
3102        })
3103    }
3104}
3105
3106/// Multiply storage by a `Complex64` scalar.
3107///
3108/// A real payload is promoted to its complex counterpart of the same width
3109/// (`f64` -> `Complex64`, `f32` -> `Complex32`); complex payloads keep their
3110/// dtype.
3111impl Mul<Complex64> for &Storage {
3112    type Output = Storage;
3113
3114    fn mul(self, scalar: Complex64) -> Self::Output {
3115        let target = if self.is_complex() {
3116            self.dtype()
3117        } else {
3118            scale_target_dtype(self.dtype(), DType::C64)
3119                .unwrap_or_else(|error| crate::invariant_violated("complex scaling", error))
3120        };
3121        storage_dtype_dispatch!(target, Result => {
3122            let values: Vec<Result> = storage_payload_dispatch!(&self.0, Source => |payload| {
3123                payload
3124                    .data()
3125                    .iter()
3126                    .map(|value| {
3127                        let (re, im) = <Source as StorageScalar>::widened_parts(*value);
3128                        <Result as StorageScalar>::from_widened_parts(
3129                            re * scalar.re - im * scalar.im,
3130                            re * scalar.im + im * scalar.re,
3131                        )
3132                    })
3133                    .collect()
3134            });
3135            rebuild_payload(
3136                self.payload_dims(),
3137                self.payload_strides(),
3138                self.axis_classes(),
3139                values,
3140            )
3141        }, else crate::invariant_violated(
3142            "complex scaling",
3143            anyhow!("unsupported storage dtype {:?}", self.dtype()),
3144        ))
3145    }
3146}
3147
3148/// Multiply storage by a dynamic [`BackendScalar`].
3149///
3150/// A real scalar delegates to the real-scaling implementation, so a complex payload's components scale
3151/// independently and IEEE values propagate as they always have (`inf * 1` stays `inf` instead of turning
3152/// into `inf * 0 = NaN` through the complex product). A complex scalar goes through the complex path,
3153/// which keeps the payload's width and gains a complex component on a real payload.
3154impl Mul<BackendScalar> for &Storage {
3155    type Output = Storage;
3156
3157    fn mul(self, scalar: BackendScalar) -> Self::Output {
3158        if scalar.is_complex() {
3159            self * Complex64::from(scalar)
3160        } else {
3161            self * scalar.real()
3162        }
3163    }
3164}
3165
3166#[cfg(test)]
3167mod tests;