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 an error when the block count does not match the shape (a shape
70    /// /// mismatch).
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 an error when the block count does not match the shape (a shape
121    /// /// mismatch).
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 an error when the blocks have inconsistent indices (a shape or
278    /// /// index mismatch).
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use tensor4all_core::block_tensor::BlockTensor;
284    /// use tensor4all_core::{DynIndex, IdxTensor};
285    ///
286    /// // Column vector: always valid regardless of index sharing
287    /// let i = DynIndex::new_dyn(2);
288    /// let j = DynIndex::new_dyn(2);
289    /// let t1 = IdxTensor::from_dense(vec![i], vec![1.0, 2.0]).unwrap();
290    /// let t2 = IdxTensor::from_dense(vec![j], vec![3.0, 4.0]).unwrap();
291    /// let bt = BlockTensor::new(vec![t1, t2], (2, 1)).unwrap();
292    /// assert!(bt.validate_indices().is_ok());
293    /// ```
294    pub fn validate_indices(&self) -> std::result::Result<(), TensorVectorSpaceError> {
295        let (rows, cols) = self.shape;
296
297        if cols <= 1 {
298            // Column vector: blocks in different rows can have independent indices.
299            // The operator determines the relationship between blocks.
300            return Ok(());
301        }
302
303        // Matrix: check all blocks have the same number of external indices
304        let first_count = self.blocks[0].num_external_indices();
305        for (i, block) in self.blocks.iter().enumerate().skip(1) {
306            let n = block.num_external_indices();
307            if n != first_count {
308                return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
309                    "Block {} has {} external indices, but block 0 has {}",
310                    i,
311                    n,
312                    first_count
313                )));
314            }
315        }
316
317        // Same row: blocks should share at least one full output index.
318        for row in 0..rows {
319            let ref_indices: HashSet<_> = self
320                .get(row, 0)
321                .ok_or_else(|| anyhow::anyhow!("block index ({row}, 0) is out of bounds"))?
322                .external_indices()
323                .iter()
324                .cloned()
325                .collect();
326            for col in 1..cols {
327                let indices: HashSet<_> = self
328                    .get(row, col)
329                    .ok_or_else(|| anyhow::anyhow!("block index ({row}, {col}) is out of bounds"))?
330                    .external_indices()
331                    .iter()
332                    .cloned()
333                    .collect();
334                let common_count = ref_indices.intersection(&indices).count();
335                if common_count == 0 {
336                    return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
337                        "Matrix row {}: blocks ({},{}) and ({},{}) share no common indices",
338                        row,
339                        row,
340                        0,
341                        row,
342                        col
343                    )));
344                }
345            }
346        }
347
348        // Same column: blocks should share at least one full input index.
349        for col in 0..cols {
350            let ref_indices: HashSet<_> = self
351                .get(0, col)
352                .ok_or_else(|| anyhow::anyhow!("block index (0, {col}) is out of bounds"))?
353                .external_indices()
354                .iter()
355                .cloned()
356                .collect();
357            for row in 1..rows {
358                let indices: HashSet<_> = self
359                    .get(row, col)
360                    .ok_or_else(|| anyhow::anyhow!("block index ({row}, {col}) is out of bounds"))?
361                    .external_indices()
362                    .iter()
363                    .cloned()
364                    .collect();
365                let common_count = ref_indices.intersection(&indices).count();
366                if common_count == 0 {
367                    return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
368                        "Matrix col {}: blocks ({},{}) and ({},{}) share no common indices",
369                        col,
370                        0,
371                        col,
372                        row,
373                        col
374                    )));
375                }
376            }
377        }
378
379        Ok(())
380    }
381}
382
383// ============================================================================
384// TensorIndex implementation
385// ============================================================================
386
387impl<T: TensorLike> TensorIndex for BlockTensor<T> {
388    type Index = T::Index;
389    type Error = TensorVectorSpaceError;
390
391    fn external_indices(&self) -> Vec<Self::Index> {
392        // Collect unique external indices across all blocks (deduplicated by full index).
393        let mut seen = HashSet::new();
394        let mut result = Vec::new();
395        for block in &self.blocks {
396            for idx in block.external_indices() {
397                if seen.insert(idx.clone()) {
398                    result.push(idx);
399                }
400            }
401        }
402        result
403    }
404
405    fn replaceind(
406        &self,
407        old_index: &Self::Index,
408        new_index: &Self::Index,
409    ) -> std::result::Result<Self, Self::Error> {
410        let replaced: std::result::Result<Vec<T>, Self::Error> = self
411            .blocks
412            .iter()
413            .map(|b| {
414                b.replaceind(old_index, new_index)
415                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
416            })
417            .collect();
418        Ok(Self {
419            blocks: replaced?,
420            shape: self.shape,
421        })
422    }
423
424    fn replace_indices(
425        &self,
426        old_indices: &[Self::Index],
427        new_indices: &[Self::Index],
428    ) -> std::result::Result<Self, Self::Error> {
429        let replaced: std::result::Result<Vec<T>, Self::Error> = self
430            .blocks
431            .iter()
432            .map(|b| {
433                b.replace_indices(old_indices, new_indices)
434                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
435            })
436            .collect();
437        Ok(Self {
438            blocks: replaced?,
439            shape: self.shape,
440        })
441    }
442}
443
444// ============================================================================
445// TensorLike implementation
446// ============================================================================
447
448impl<T: TensorLike> TensorVectorSpace for BlockTensor<T> {
449    // ------------------------------------------------------------------------
450    // Vector space operations (required for GMRES)
451    // ------------------------------------------------------------------------
452
453    fn norm_squared(&self) -> std::result::Result<f64, Self::Error> {
454        self.blocks.iter().try_fold(0.0, |sum, block| {
455            let value = block
456                .norm_squared()
457                .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))?;
458            Ok(sum + value)
459        })
460    }
461
462    fn maxabs(&self) -> std::result::Result<f64, Self::Error> {
463        self.blocks.iter().try_fold(0.0_f64, |acc, block| {
464            let value = block
465                .maxabs()
466                .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))?;
467            Ok(acc.max(value))
468        })
469    }
470
471    fn scale(&self, scalar: AnyScalar) -> std::result::Result<Self, Self::Error> {
472        let scaled: std::result::Result<Vec<T>, Self::Error> = self
473            .blocks
474            .iter()
475            .map(|b| {
476                b.scale(scalar.clone())
477                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
478            })
479            .collect();
480        Ok(Self {
481            blocks: scaled?,
482            shape: self.shape,
483        })
484    }
485
486    fn axpby(
487        &self,
488        a: AnyScalar,
489        other: &Self,
490        b: AnyScalar,
491    ) -> std::result::Result<Self, Self::Error> {
492        if self.shape != other.shape {
493            return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
494                "Block shapes must match: {:?} vs {:?}",
495                self.shape,
496                other.shape
497            )));
498        }
499        let result: std::result::Result<Vec<T>, Self::Error> = self
500            .blocks
501            .iter()
502            .zip(other.blocks.iter())
503            .map(|(s, o)| {
504                s.axpby(a.clone(), o, b.clone())
505                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
506            })
507            .collect();
508        Ok(Self {
509            blocks: result?,
510            shape: self.shape,
511        })
512    }
513
514    fn inner_product(&self, other: &Self) -> std::result::Result<AnyScalar, Self::Error> {
515        if self.shape != other.shape {
516            return Err(TensorVectorSpaceError::from(anyhow::anyhow!(
517                "Block shapes must match for inner product: {:?} vs {:?}",
518                self.shape,
519                other.shape
520            )));
521        }
522        let mut sum = AnyScalar::new_real(0.0);
523        for (s, o) in self.blocks.iter().zip(other.blocks.iter()) {
524            let value = s
525                .inner_product(o)
526                .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))?;
527            sum = sum + value;
528        }
529        Ok(sum)
530    }
531
532    fn validate(&self) -> std::result::Result<(), Self::Error> {
533        self.validate_indices()
534    }
535}
536
537impl<T: TensorLike> TensorContractionLike for BlockTensor<T> {
538    // ------------------------------------------------------------------------
539    // Tensor network operations
540    // ------------------------------------------------------------------------
541
542    fn conj(&self) -> Self {
543        let conjugated: Vec<T> = self.blocks.iter().map(|b| b.conj()).collect();
544        Self {
545            blocks: conjugated,
546            shape: self.shape,
547        }
548    }
549
550    fn direct_sum(
551        &self,
552        _other: &Self,
553        _pairs: &[(<Self as TensorIndex>::Index, <Self as TensorIndex>::Index)],
554    ) -> std::result::Result<DirectSumResult<Self>, Self::Error> {
555        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
556            "BlockTensor does not support direct_sum"
557        )))
558    }
559
560    fn outer_product(&self, _other: &Self) -> std::result::Result<Self, Self::Error> {
561        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
562            "BlockTensor does not support outer_product"
563        )))
564    }
565
566    fn permuteinds(
567        &self,
568        _new_order: &[<Self as TensorIndex>::Index],
569    ) -> std::result::Result<Self, Self::Error> {
570        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
571            "BlockTensor does not support permuteinds"
572        )))
573    }
574
575    fn fuse_indices(
576        &self,
577        old_indices: &[Self::Index],
578        new_index: Self::Index,
579        order: LinearizationOrder,
580    ) -> std::result::Result<Self, Self::Error> {
581        let blocks: std::result::Result<Vec<T>, Self::Error> = self
582            .blocks
583            .iter()
584            .map(|block| {
585                block
586                    .fuse_indices(old_indices, new_index.clone(), order)
587                    .map_err(|error| TensorVectorSpaceError::from(anyhow::Error::new(error)))
588            })
589            .collect();
590        Ok(Self {
591            blocks: blocks?,
592            shape: self.shape,
593        })
594    }
595
596    fn contract(_tensors: &[&Self]) -> std::result::Result<Self, Self::Error> {
597        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
598            "BlockTensor does not support contract"
599        )))
600    }
601}
602
603impl<T: TensorLike> TensorFactorizationLike for BlockTensor<T> {
604    fn factorize(
605        &self,
606        _left_inds: &[<Self as TensorIndex>::Index],
607        _options: &FactorizeOptions,
608    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError> {
609        Err(FactorizeError::ComputationError(anyhow::anyhow!(
610            "BlockTensor does not support factorize"
611        )))
612    }
613
614    fn factorize_full_rank(
615        &self,
616        _left_inds: &[<Self as TensorIndex>::Index],
617        _alg: crate::FactorizeAlg,
618        _canonical: crate::Canonical,
619    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError> {
620        Err(FactorizeError::ComputationError(anyhow::anyhow!(
621            "BlockTensor does not support factorize_full_rank"
622        )))
623    }
624}
625
626impl<T: TensorLike> TensorConstructionLike for BlockTensor<T> {
627    fn diagonal(
628        _input_index: &<Self as TensorIndex>::Index,
629        _output_index: &<Self as TensorIndex>::Index,
630    ) -> std::result::Result<Self, Self::Error> {
631        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
632            "BlockTensor does not support diagonal"
633        )))
634    }
635
636    fn scalar_one() -> std::result::Result<Self, Self::Error> {
637        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
638            "BlockTensor does not support scalar_one"
639        )))
640    }
641
642    fn ones(_indices: &[<Self as TensorIndex>::Index]) -> std::result::Result<Self, Self::Error> {
643        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
644            "BlockTensor does not support ones"
645        )))
646    }
647
648    fn onehot(
649        _index_vals: &[(<Self as TensorIndex>::Index, usize)],
650    ) -> std::result::Result<Self, Self::Error> {
651        Err(TensorVectorSpaceError::from(anyhow::anyhow!(
652            "BlockTensor does not support onehot"
653        )))
654    }
655}
656
657#[cfg(test)]
658mod tests;