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;