Skip to main content

tensor4all_core/
block_tensor.rs

1//! Block tensor type for GMRES with block matrices.
2//!
3//! This module provides [`BlockTensor`], a collection of tensors organized
4//! in a block structure. It implements [`TensorLike`] for the vector space
5//! operations required by GMRES, allowing block matrix linear equations
6//! `Ax = b` to be solved using the existing GMRES implementation.
7//!
8//! # Example
9//!
10//! ```
11//! use tensor4all_core::block_tensor::BlockTensor;
12//! use tensor4all_core::krylov::{gmres, GmresOptions};
13//! use tensor4all_core::{DynIndex, IdxTensor};
14//!
15//! # fn main() -> anyhow::Result<()> {
16//! let i = DynIndex::new_dyn(2);
17//! let b_block = IdxTensor::from_dense(vec![i.clone()], vec![1.0, 2.0])?;
18//! let zero_block = IdxTensor::from_dense(vec![i.clone()], vec![0.0, 0.0])?;
19//!
20//! let b = BlockTensor::new(vec![b_block], (1, 1))?;
21//! let x0 = BlockTensor::new(vec![zero_block], (1, 1))?;
22//!
23//! let apply_a = |x: &BlockTensor<IdxTensor>| Ok(x.clone());
24//! let result = gmres(apply_a, &b, &x0, &GmresOptions::default())?;
25//!
26//! assert!(result.converged);
27//! assert_eq!(result.solution.shape(), (1, 1));
28//! # Ok(())
29//! # }
30//! ```
31
32use std::collections::HashSet;
33
34use crate::any_scalar::AnyScalar;
35use crate::tensor_index::TensorIndex;
36use crate::tensor_like::{
37    DirectSumResult, FactorizeError, FactorizeOptions, FactorizeResult, LinearizationOrder,
38    TensorConstructionLike, TensorContractionLike, TensorFactorizationLike, TensorLike,
39    TensorVectorSpace, TensorVectorSpaceError,
40};
41
42/// A collection of tensors organized in a block structure.
43///
44/// Each block is a tensor of type `T` implementing [`TensorLike`].
45/// The flattened block list is ordered row-by-row:
46/// `(0, 0), (0, 1), ..., (1, 0), (1, 1), ...`.
47///
48/// # Type Parameters
49///
50/// * `T` - The tensor type for each block, must implement `TensorLike`
51#[derive(Debug, Clone)]
52pub struct BlockTensor<T: TensorLike> {
53    /// Blocks flattened row-by-row in block-matrix order
54    blocks: Vec<T>,
55    /// Block structure (rows, cols)
56    shape: (usize, usize),
57}
58
59impl<T: TensorLike> BlockTensor<T> {
60    /// Create a new block tensor with validation.
61    ///
62    /// # Arguments
63    ///
64    /// * `blocks` - Vector of blocks flattened row-by-row
65    /// * `shape` - Block structure as (rows, cols)
66    ///
67    /// # Errors
68    ///
69    /// Returns [`TensorVectorSpaceError`] when `rows * cols` overflows `usize`
70    /// or when `blocks.len()` differs from `rows * cols`.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use tensor4all_core::block_tensor::BlockTensor;
76    /// use tensor4all_core::{DynIndex, IdxTensor};
77    ///
78    /// let i = DynIndex::new_dyn(2);
79    /// let t1 = IdxTensor::from_dense(vec![i.clone()], vec![1.0, 2.0]).unwrap();
80    /// let t2 = IdxTensor::from_dense(vec![i.clone()], vec![3.0, 4.0]).unwrap();
81    ///
82    /// let bt = BlockTensor::try_new(vec![t1, t2], (2, 1)).unwrap();
83    /// assert_eq!(bt.shape(), (2, 1));
84    ///
85    /// // Wrong number of blocks returns an error
86    /// let t3 = IdxTensor::from_dense(vec![i], vec![5.0, 6.0]).unwrap();
87    /// assert!(BlockTensor::try_new(vec![t3], (2, 1)).is_err());
88    /// ```
89    pub fn try_new(
90        blocks: Vec<T>,
91        shape: (usize, usize),
92    ) -> std::result::Result<Self, TensorVectorSpaceError> {
93        let (rows, cols) = shape;
94        let expected = rows.checked_mul(cols).ok_or_else(|| {
95            TensorVectorSpaceError::from(anyhow::anyhow!(
96                "Block count shape ({rows}, {cols}) overflows usize"
97            ))
98        })?;
99        if expected != blocks.len() {
100            return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
101                "Block count mismatch: shape ({}, {}) requires {} blocks, but got {}",
102                rows,
103                cols,
104                expected,
105                blocks.len()
106            )));
107        }
108        Ok(Self { blocks, shape })
109    }
110
111    /// Create a new block tensor.
112    ///
113    /// # Arguments
114    ///
115    /// * `blocks` - Vector of blocks flattened row-by-row
116    /// * `shape` - Block structure as (rows, cols)
117    ///
118    /// # Errors
119    ///
120    /// Returns [`TensorVectorSpaceError`] when `rows * cols` overflows `usize`
121    /// or when `blocks.len()` differs from `rows * cols`.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use tensor4all_core::block_tensor::BlockTensor;
127    /// use tensor4all_core::{DynIndex, IdxTensor};
128    ///
129    /// let i = DynIndex::new_dyn(2);
130    /// let t = IdxTensor::from_dense(vec![i], vec![1.0, 2.0]).unwrap();
131    /// let bt = BlockTensor::new(vec![t], (1, 1)).unwrap();
132    /// assert_eq!(bt.shape(), (1, 1));
133    /// ```
134    pub fn new(
135        blocks: Vec<T>,
136        shape: (usize, usize),
137    ) -> std::result::Result<Self, TensorVectorSpaceError> {
138        Self::try_new(blocks, shape)
139    }
140
141    /// Get the block structure (rows, cols).
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use tensor4all_core::block_tensor::BlockTensor;
147    /// use tensor4all_core::{DynIndex, IdxTensor};
148    ///
149    /// let i = DynIndex::new_dyn(2);
150    /// let blocks: Vec<IdxTensor> = (0..6)
151    ///     .map(|_| IdxTensor::from_dense(vec![i.clone()], vec![0.0, 0.0]).unwrap())
152    ///     .collect();
153    /// let bt = BlockTensor::new(blocks, (2, 3)).unwrap();
154    /// assert_eq!(bt.shape(), (2, 3));
155    /// ```
156    pub fn shape(&self) -> (usize, usize) {
157        self.shape
158    }
159
160    /// Get the total number of blocks.
161    pub fn num_blocks(&self) -> usize {
162        self.blocks.len()
163    }
164
165    /// Get a reference to the block at (row, col).
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use tensor4all_core::block_tensor::BlockTensor;
171    /// use tensor4all_core::{DynIndex, IdxTensor};
172    ///
173    /// let i = DynIndex::new_dyn(2);
174    /// let t1 = IdxTensor::from_dense(vec![i.clone()], vec![1.0, 2.0]).unwrap();
175    /// let t2 = IdxTensor::from_dense(vec![i], vec![3.0, 4.0]).unwrap();
176    /// let bt = BlockTensor::new(vec![t1, t2], (2, 1)).unwrap();
177    /// assert_eq!(bt.get(0, 0).unwrap().dims(), vec![2]);
178    /// assert_eq!(bt.get(1, 0).unwrap().dims(), vec![2]);
179    /// ```
180    pub fn get(&self, row: usize, col: usize) -> Option<&T> {
181        let (rows, cols) = self.shape;
182        if row >= rows || col >= cols {
183            return None;
184        }
185        self.blocks.get(row * cols + col)
186    }
187
188    /// Get a mutable reference to the block at (row, col).
189    ///
190    /// # Examples
191    ///
192    /// ```
193    /// use tensor4all_core::block_tensor::BlockTensor;
194    /// use tensor4all_core::{DynIndex, IdxTensor};
195    ///
196    /// let i = DynIndex::new_dyn(2);
197    /// let t = IdxTensor::from_dense(vec![i.clone()], vec![1.0, 2.0]).unwrap();
198    /// let mut bt = BlockTensor::new(vec![t], (1, 1)).unwrap();
199    /// let block = bt.get_mut(0, 0).unwrap();
200    /// assert_eq!(block.dims(), vec![2]);
201    /// ```
202    pub fn get_mut(&mut self, row: usize, col: usize) -> Option<&mut T> {
203        let (rows, cols) = self.shape;
204        if row >= rows || col >= cols {
205            return None;
206        }
207        self.blocks.get_mut(row * cols + col)
208    }
209
210    /// Get all blocks as a slice.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use tensor4all_core::block_tensor::BlockTensor;
216    /// use tensor4all_core::{DynIndex, IdxTensor};
217    ///
218    /// let i = DynIndex::new_dyn(2);
219    /// let t1 = IdxTensor::from_dense(vec![i.clone()], vec![1.0, 2.0]).unwrap();
220    /// let t2 = IdxTensor::from_dense(vec![i], vec![3.0, 4.0]).unwrap();
221    /// let bt = BlockTensor::new(vec![t1, t2], (1, 2)).unwrap();
222    /// assert_eq!(bt.blocks().len(), 2);
223    /// ```
224    pub fn blocks(&self) -> &[T] {
225        &self.blocks
226    }
227
228    /// Get all blocks as a mutable slice.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use tensor4all_core::block_tensor::BlockTensor;
234    /// use tensor4all_core::{DynIndex, IdxTensor};
235    ///
236    /// let i = DynIndex::new_dyn(2);
237    /// let t = IdxTensor::from_dense(vec![i], vec![1.0, 2.0]).unwrap();
238    /// let mut bt = BlockTensor::new(vec![t], (1, 1)).unwrap();
239    /// assert_eq!(bt.blocks_mut().len(), 1);
240    /// ```
241    pub fn blocks_mut(&mut self) -> &mut [T] {
242        &mut self.blocks
243    }
244
245    /// Consume self and return the blocks.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use tensor4all_core::block_tensor::BlockTensor;
251    /// use tensor4all_core::{DynIndex, IdxTensor};
252    ///
253    /// let i = DynIndex::new_dyn(2);
254    /// let t = IdxTensor::from_dense(vec![i], vec![1.0, 2.0]).unwrap();
255    /// let bt = BlockTensor::new(vec![t], (1, 1)).unwrap();
256    /// let blocks = bt.into_blocks();
257    /// assert_eq!(blocks.len(), 1);
258    /// assert_eq!(blocks[0].dims(), vec![2]);
259    /// ```
260    pub fn into_blocks(self) -> Vec<T> {
261        self.blocks
262    }
263
264    /// Validate that blocks share external indices consistently.
265    ///
266    /// For column vectors (cols=1), no index sharing is required between
267    /// blocks. Different rows can have independent physical indices
268    /// (the operator determines their relationship).
269    ///
270    /// For matrices (rows x cols), checks that:
271    /// - All blocks have the same number of external indices.
272    /// - Blocks in the same row share some common index IDs (output indices).
273    /// - Blocks in the same column share some common index IDs (input indices).
274    ///
275    /// # Errors
276    ///
277    /// Returns [`TensorVectorSpaceError`] when blocks disagree on their number
278    /// of external indices, or when blocks in a common row or column share no
279    /// index.
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use tensor4all_core::block_tensor::BlockTensor;
285    /// use tensor4all_core::{DynIndex, IdxTensor};
286    ///
287    /// // Column vector: always valid regardless of index sharing
288    /// let i = DynIndex::new_dyn(2);
289    /// let j = DynIndex::new_dyn(2);
290    /// let t1 = IdxTensor::from_dense(vec![i], vec![1.0, 2.0]).unwrap();
291    /// let t2 = IdxTensor::from_dense(vec![j], vec![3.0, 4.0]).unwrap();
292    /// let bt = BlockTensor::new(vec![t1, t2], (2, 1)).unwrap();
293    /// assert!(bt.validate_indices().is_ok());
294    /// ```
295    pub fn validate_indices(&self) -> std::result::Result<(), TensorVectorSpaceError> {
296        let (rows, cols) = self.shape;
297
298        if cols <= 1 {
299            // Column vector: blocks in different rows can have independent indices.
300            // The operator determines the relationship between blocks.
301            return Ok(());
302        }
303
304        // Matrix: check all blocks have the same number of external indices
305        let first_count = self.blocks[0].num_external_indices();
306        for (i, block) in self.blocks.iter().enumerate().skip(1) {
307            let n = block.num_external_indices();
308            if n != first_count {
309                return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
310                    "Block {} has {} external indices, but block 0 has {}",
311                    i,
312                    n,
313                    first_count
314                )));
315            }
316        }
317
318        // Same row: blocks should share at least one full output index.
319        for row in 0..rows {
320            let ref_indices: HashSet<_> = self
321                .get(row, 0)
322                .ok_or_else(|| anyhow::anyhow!("block index ({row}, 0) is out of bounds"))?
323                .external_indices()
324                .iter()
325                .cloned()
326                .collect();
327            for col in 1..cols {
328                let indices: HashSet<_> = self
329                    .get(row, col)
330                    .ok_or_else(|| anyhow::anyhow!("block index ({row}, {col}) is out of bounds"))?
331                    .external_indices()
332                    .iter()
333                    .cloned()
334                    .collect();
335                let common_count = ref_indices.intersection(&indices).count();
336                if common_count == 0 {
337                    return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
338                        "Matrix row {}: blocks ({},{}) and ({},{}) share no common indices",
339                        row,
340                        row,
341                        0,
342                        row,
343                        col
344                    )));
345                }
346            }
347        }
348
349        // Same column: blocks should share at least one full input index.
350        for col in 0..cols {
351            let ref_indices: HashSet<_> = self
352                .get(0, col)
353                .ok_or_else(|| anyhow::anyhow!("block index (0, {col}) is out of bounds"))?
354                .external_indices()
355                .iter()
356                .cloned()
357                .collect();
358            for row in 1..rows {
359                let indices: HashSet<_> = self
360                    .get(row, col)
361                    .ok_or_else(|| anyhow::anyhow!("block index ({row}, {col}) is out of bounds"))?
362                    .external_indices()
363                    .iter()
364                    .cloned()
365                    .collect();
366                let common_count = ref_indices.intersection(&indices).count();
367                if common_count == 0 {
368                    return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
369                        "Matrix col {}: blocks ({},{}) and ({},{}) share no common indices",
370                        col,
371                        0,
372                        col,
373                        row,
374                        col
375                    )));
376                }
377            }
378        }
379
380        Ok(())
381    }
382}
383
384// ============================================================================
385// TensorIndex implementation
386// ============================================================================
387
388impl<T: TensorLike> TensorIndex for BlockTensor<T> {
389    type Index = T::Index;
390    type Error = TensorVectorSpaceError;
391
392    fn external_indices(&self) -> Vec<Self::Index> {
393        // Collect unique external indices across all blocks (deduplicated by full index).
394        let mut seen = HashSet::new();
395        let mut result = Vec::new();
396        for block in &self.blocks {
397            for idx in block.external_indices() {
398                if seen.insert(idx.clone()) {
399                    result.push(idx);
400                }
401            }
402        }
403        result
404    }
405
406    fn replaceind(
407        &self,
408        old_index: &Self::Index,
409        new_index: &Self::Index,
410    ) -> std::result::Result<Self, Self::Error> {
411        let replaced: std::result::Result<Vec<T>, Self::Error> = self
412            .blocks
413            .iter()
414            .map(|b| {
415                b.replaceind(old_index, new_index)
416                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
417            })
418            .collect();
419        Ok(Self {
420            blocks: replaced?,
421            shape: self.shape,
422        })
423    }
424
425    fn replace_indices(
426        &self,
427        old_indices: &[Self::Index],
428        new_indices: &[Self::Index],
429    ) -> std::result::Result<Self, Self::Error> {
430        let replaced: std::result::Result<Vec<T>, Self::Error> = self
431            .blocks
432            .iter()
433            .map(|b| {
434                b.replace_indices(old_indices, new_indices)
435                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
436            })
437            .collect();
438        Ok(Self {
439            blocks: replaced?,
440            shape: self.shape,
441        })
442    }
443}
444
445// ============================================================================
446// TensorLike implementation
447// ============================================================================
448
449impl<T: TensorLike> TensorVectorSpace for BlockTensor<T> {
450    // ------------------------------------------------------------------------
451    // Vector space operations (required for GMRES)
452    // ------------------------------------------------------------------------
453
454    fn norm_squared(&self) -> std::result::Result<f64, Self::Error> {
455        self.blocks.iter().try_fold(0.0, |sum, block| {
456            let value = block
457                .norm_squared()
458                .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))?;
459            Ok(sum + value)
460        })
461    }
462
463    fn maxabs(&self) -> std::result::Result<f64, Self::Error> {
464        self.blocks.iter().try_fold(0.0_f64, |acc, block| {
465            let value = block
466                .maxabs()
467                .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))?;
468            Ok(acc.max(value))
469        })
470    }
471
472    fn scale(&self, scalar: AnyScalar) -> std::result::Result<Self, Self::Error> {
473        let scaled: std::result::Result<Vec<T>, Self::Error> = self
474            .blocks
475            .iter()
476            .map(|b| {
477                b.scale(scalar.clone())
478                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
479            })
480            .collect();
481        Ok(Self {
482            blocks: scaled?,
483            shape: self.shape,
484        })
485    }
486
487    fn axpby(
488        &self,
489        a: AnyScalar,
490        other: &Self,
491        b: AnyScalar,
492    ) -> std::result::Result<Self, Self::Error> {
493        if self.shape != other.shape {
494            return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
495                "Block shapes must match: {:?} vs {:?}",
496                self.shape,
497                other.shape
498            )));
499        }
500        let result: std::result::Result<Vec<T>, Self::Error> = self
501            .blocks
502            .iter()
503            .zip(other.blocks.iter())
504            .map(|(s, o)| {
505                s.axpby(a.clone(), o, b.clone())
506                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
507            })
508            .collect();
509        Ok(Self {
510            blocks: result?,
511            shape: self.shape,
512        })
513    }
514
515    fn inner_product(&self, other: &Self) -> std::result::Result<AnyScalar, Self::Error> {
516        if self.shape != other.shape {
517            return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
518                "Block shapes must match for inner product: {:?} vs {:?}",
519                self.shape,
520                other.shape
521            )));
522        }
523        let mut sum = AnyScalar::new_real(0.0);
524        for (s, o) in self.blocks.iter().zip(other.blocks.iter()) {
525            let value = s
526                .inner_product(o)
527                .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))?;
528            sum = sum + value;
529        }
530        Ok(sum)
531    }
532
533    fn validate(&self) -> std::result::Result<(), Self::Error> {
534        self.validate_indices()
535    }
536}
537
538impl<T: TensorLike> TensorContractionLike for BlockTensor<T> {
539    // ------------------------------------------------------------------------
540    // Tensor network operations
541    // ------------------------------------------------------------------------
542
543    fn conj(&self) -> Self {
544        let conjugated: Vec<T> = self.blocks.iter().map(|b| b.conj()).collect();
545        Self {
546            blocks: conjugated,
547            shape: self.shape,
548        }
549    }
550
551    fn direct_sum(
552        &self,
553        _other: &Self,
554        _pairs: &[(<Self as TensorIndex>::Index, <Self as TensorIndex>::Index)],
555    ) -> std::result::Result<DirectSumResult<Self>, Self::Error> {
556        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
557            "BlockTensor does not support direct_sum"
558        )))
559    }
560
561    fn outer_product(&self, _other: &Self) -> std::result::Result<Self, Self::Error> {
562        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
563            "BlockTensor does not support outer_product"
564        )))
565    }
566
567    fn permuteinds(
568        &self,
569        _new_order: &[<Self as TensorIndex>::Index],
570    ) -> std::result::Result<Self, Self::Error> {
571        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
572            "BlockTensor does not support permuteinds"
573        )))
574    }
575
576    fn fuse_indices(
577        &self,
578        old_indices: &[Self::Index],
579        new_index: Self::Index,
580        order: LinearizationOrder,
581    ) -> std::result::Result<Self, Self::Error> {
582        let blocks: std::result::Result<Vec<T>, Self::Error> = self
583            .blocks
584            .iter()
585            .map(|block| {
586                block
587                    .fuse_indices(old_indices, new_index.clone(), order)
588                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
589            })
590            .collect();
591        Ok(Self {
592            blocks: blocks?,
593            shape: self.shape,
594        })
595    }
596
597    fn contract(_tensors: &[&Self]) -> std::result::Result<Self, Self::Error> {
598        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
599            "BlockTensor does not support contract"
600        )))
601    }
602}
603
604impl<T: TensorLike> TensorFactorizationLike for BlockTensor<T> {
605    fn factorize(
606        &self,
607        _left_inds: &[<Self as TensorIndex>::Index],
608        _options: &FactorizeOptions,
609    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError> {
610        Err(FactorizeError::ComputationError(anyhow::anyhow!(
611            "BlockTensor does not support factorize"
612        )))
613    }
614
615    fn factorize_full_rank(
616        &self,
617        _left_inds: &[<Self as TensorIndex>::Index],
618        _alg: crate::FactorizeAlg,
619        _canonical: crate::Canonical,
620    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError> {
621        Err(FactorizeError::ComputationError(anyhow::anyhow!(
622            "BlockTensor does not support factorize_full_rank"
623        )))
624    }
625}
626
627impl<T: TensorLike> TensorConstructionLike for BlockTensor<T> {
628    fn diagonal(
629        _input_index: &<Self as TensorIndex>::Index,
630        _output_index: &<Self as TensorIndex>::Index,
631    ) -> std::result::Result<Self, Self::Error> {
632        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
633            "BlockTensor does not support diagonal"
634        )))
635    }
636
637    fn scalar_one() -> std::result::Result<Self, Self::Error> {
638        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
639            "BlockTensor does not support scalar_one"
640        )))
641    }
642
643    fn ones(_indices: &[<Self as TensorIndex>::Index]) -> std::result::Result<Self, Self::Error> {
644        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
645            "BlockTensor does not support ones"
646        )))
647    }
648
649    fn onehot(
650        _index_vals: &[(<Self as TensorIndex>::Index, usize)],
651    ) -> std::result::Result<Self, Self::Error> {
652        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
653            "BlockTensor does not support onehot"
654        )))
655    }
656}
657
658#[cfg(test)]
659mod tests;