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