Skip to main content

tensor4all_core/defaults/
direct_sum.rs

1//! Direct sum operations for tensors.
2//!
3//! This module provides functionality to compute the direct sum of tensors
4//! along specified index pairs. The direct sum concatenates tensor data along
5//! the paired indices, creating new indices with combined dimensions.
6//!
7//! This module works with concrete types (`DynIndex`, `IdxTensor`) only.
8
9use crate::defaults::DynIndex;
10use crate::defaults::IdxTensorError;
11use crate::index_like::IndexLike;
12use crate::tensor::IdxTensor;
13use anyhow::Result;
14use num_traits::Zero;
15use tensor4all_tensorbackend::TensorElement;
16
17/// Compute the direct sum of two tensors along specified index pairs.
18///
19/// For tensors A and B with indices to be summed specified as pairs,
20/// creates a new tensor C where each paired index has dimension = dim_A + dim_B.
21/// Non-paired indices must match exactly between A and B.
22///
23/// # Arguments
24///
25/// * `a` - First tensor
26/// * `b` - Second tensor
27/// * `pairs` - Pairs of (a_index, b_index) to be summed. Each pair creates
28///
29///   a new index in the result with dimension = dim(a_index) + dim(b_index).
30///
31/// # Returns
32///
33/// A tuple of:
34/// - The direct sum tensor
35/// - The new indices created for the summed dimensions (one per pair)
36///
37/// # Errors
38///
39/// Returns [`IdxTensorError::Operation`] when `pairs` is empty, when a paired
40/// or common index does not match between the operands, when the operands have
41/// different dense scalar types, or when the backend construction fails.
42///
43/// # Example
44///
45/// ```
46/// use tensor4all_core::{direct_sum, DynIndex, IdxTensor};
47///
48/// # fn main() -> anyhow::Result<()> {
49/// let j = DynIndex::new_dyn(2);
50/// let k = DynIndex::new_dyn(3);
51///
52/// let a = IdxTensor::from_dense(vec![j.clone()], vec![1.0, 2.0])?;
53/// let b = IdxTensor::from_dense(vec![k.clone()], vec![3.0, 4.0, 5.0])?;
54/// let (result, new_indices) = direct_sum(&a, &b, &[(j.clone(), k.clone())])?;
55///
56/// assert_eq!(new_indices.len(), 1);
57/// assert_eq!(result.to_vec::<f64>()?, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
58/// # Ok(())
59/// # }
60/// ```
61pub fn direct_sum(
62    a: &IdxTensor,
63    b: &IdxTensor,
64    pairs: &[(DynIndex, DynIndex)],
65) -> std::result::Result<(IdxTensor, Vec<DynIndex>), IdxTensorError> {
66    if a.is_f64() && b.is_f64() {
67        direct_sum_typed::<f64>(a, b, pairs).map_err(IdxTensorError::from)
68    } else if a.is_complex() && b.is_complex() {
69        direct_sum_typed::<num_complex::Complex64>(a, b, pairs).map_err(IdxTensorError::from)
70    } else {
71        Err(IdxTensorError::Operation {
72            operation: "direct_sum",
73            source: std::sync::Arc::new(std::io::Error::other(
74                "direct_sum requires both tensors to have the same dense scalar type (f64 or Complex64)",
75            )),
76        })
77    }
78}
79
80/// Setup data for direct sum computation
81struct DirectSumSetup {
82    common_a_positions: Vec<usize>,
83    common_b_positions: Vec<usize>,
84    paired_a_positions: Vec<usize>,
85    paired_b_positions: Vec<usize>,
86    paired_dims_a: Vec<usize>,
87    result_indices: Vec<DynIndex>,
88    result_dims: Vec<usize>,
89    result_total: usize,
90    a_strides: Vec<usize>,
91    b_strides: Vec<usize>,
92    new_indices: Vec<DynIndex>,
93    n_common: usize,
94}
95
96fn setup_direct_sum(
97    a: &IdxTensor,
98    b: &IdxTensor,
99    pairs: &[(DynIndex, DynIndex)],
100) -> Result<DirectSumSetup> {
101    use std::collections::HashMap;
102
103    if pairs.is_empty() {
104        return Err(anyhow::anyhow!(
105            "direct_sum requires at least one index pair"
106        ));
107    }
108
109    // Build maps from full index metadata to positions. Same-id indices that
110    // differ by prime level or tags are distinct axes.
111    let a_idx_map: HashMap<&DynIndex, usize> = a
112        .indices
113        .iter()
114        .enumerate()
115        .map(|(i, idx)| (idx, i))
116        .collect();
117    let b_idx_map: HashMap<&DynIndex, usize> = b
118        .indices
119        .iter()
120        .enumerate()
121        .map(|(i, idx)| (idx, i))
122        .collect();
123
124    // Build set of paired indices.
125    let mut a_paired_indices: std::collections::HashSet<&DynIndex> =
126        std::collections::HashSet::new();
127    let mut b_paired_indices: std::collections::HashSet<&DynIndex> =
128        std::collections::HashSet::new();
129
130    for (a_idx, b_idx) in pairs {
131        if !a_idx_map.contains_key(a_idx) {
132            return Err(anyhow::anyhow!(
133                "Index not found in first tensor (direct_sum)"
134            ));
135        }
136        if !b_idx_map.contains_key(b_idx) {
137            return Err(anyhow::anyhow!(
138                "Index not found in second tensor (direct_sum)"
139            ));
140        }
141        a_paired_indices.insert(a_idx);
142        b_paired_indices.insert(b_idx);
143    }
144
145    // Identify common (non-paired) indices
146    // Iterate over a.indices (Vec) to preserve deterministic order,
147    // using b_idx_map only for lookups.
148    let mut common_a_positions: Vec<usize> = Vec::new();
149    let mut common_b_positions: Vec<usize> = Vec::new();
150    for (a_pos, a_idx) in a.indices.iter().enumerate() {
151        if a_paired_indices.contains(a_idx) {
152            continue;
153        }
154        if let Some(&b_pos) = b_idx_map.get(a_idx) {
155            if b_paired_indices.contains(a_idx) {
156                continue;
157            }
158            let a_dims = a.dims();
159            let b_dims = b.dims();
160            if a_dims[a_pos] != b_dims[b_pos] {
161                return Err(anyhow::anyhow!(
162                    "Dimension mismatch for common index: {} vs {}",
163                    a_dims[a_pos],
164                    b_dims[b_pos]
165                ));
166            }
167            common_a_positions.push(a_pos);
168            common_b_positions.push(b_pos);
169        }
170    }
171
172    // Build paired positions and dimensions
173    let paired_a_positions: Vec<usize> = pairs.iter().map(|(a_idx, _)| a_idx_map[a_idx]).collect();
174    let paired_b_positions: Vec<usize> = pairs.iter().map(|(_, b_idx)| b_idx_map[b_idx]).collect();
175    let a_dims = a.dims();
176    let b_dims = b.dims();
177    let paired_dims_a: Vec<usize> = paired_a_positions.iter().map(|&p| a_dims[p]).collect();
178    let paired_dims_b: Vec<usize> = paired_b_positions.iter().map(|&p| b_dims[p]).collect();
179
180    // Create new indices for paired dimensions
181    let mut new_indices: Vec<DynIndex> = Vec::new();
182    for (&dim_a, &dim_b) in paired_dims_a.iter().zip(&paired_dims_b) {
183        let new_dim = dim_a
184            .checked_add(dim_b)
185            .ok_or_else(|| anyhow::anyhow!("direct_sum index dimension overflowed usize"))?;
186        let new_index = DynIndex::new_link(new_dim)
187            .map_err(|e| anyhow::anyhow!("Failed to create index: {:?}", e))?;
188        new_indices.push(new_index);
189    }
190
191    // Build result indices and dimensions
192    let mut result_indices: Vec<DynIndex> = Vec::new();
193    let mut result_dims: Vec<usize> = Vec::new();
194
195    // Common indices first.
196    let a_dims = a.dims();
197    for &a_pos in &common_a_positions {
198        result_indices.push(a.indices[a_pos].clone());
199        result_dims.push(a_dims[a_pos]);
200    }
201
202    // New indices from pairs preserve the full index identity semantics (id + plev + tags).
203    for new_idx in &new_indices {
204        result_indices.push(new_idx.clone());
205        result_dims.push(new_idx.dim());
206    }
207
208    // Compute column-major strides (leftmost index fastest).
209    let result_total = checked_product(&result_dims, "direct_sum result")?;
210
211    let a_dims = a.dims();
212    let b_dims = b.dims();
213    let a_strides = checked_strides(&a_dims, "direct_sum lhs")?;
214    let b_strides = checked_strides(&b_dims, "direct_sum rhs")?;
215
216    let n_common = common_a_positions.len();
217
218    Ok(DirectSumSetup {
219        common_a_positions,
220        common_b_positions,
221        paired_a_positions,
222        paired_b_positions,
223        paired_dims_a,
224        result_indices,
225        result_dims,
226        result_total,
227        a_strides,
228        b_strides,
229        new_indices,
230        n_common,
231    })
232}
233
234fn checked_product(dims: &[usize], name: &str) -> Result<usize> {
235    dims.iter().try_fold(1usize, |acc, &dim| {
236        acc.checked_mul(dim)
237            .ok_or_else(|| anyhow::anyhow!("{name} shape product overflowed usize"))
238    })
239}
240
241fn checked_strides(dims: &[usize], name: &str) -> Result<Vec<usize>> {
242    let mut strides = vec![1usize; dims.len()];
243    for i in 1..dims.len() {
244        strides[i] = strides[i - 1]
245            .checked_mul(dims[i - 1])
246            .ok_or_else(|| anyhow::anyhow!("{name} stride overflowed usize"))?;
247    }
248    Ok(strides)
249}
250
251fn linear_to_multi(linear: usize, dims: &[usize]) -> Vec<usize> {
252    let mut multi = vec![0; dims.len()];
253    let mut remaining = linear;
254    for i in 0..dims.len() {
255        multi[i] = remaining % dims[i];
256        remaining /= dims[i];
257    }
258    multi
259}
260
261fn multi_to_linear(multi: &[usize], strides: &[usize]) -> Result<usize> {
262    multi.iter().zip(strides).try_fold(0usize, |acc, (&m, &s)| {
263        let offset = m
264            .checked_mul(s)
265            .ok_or_else(|| anyhow::anyhow!("direct_sum flat offset overflowed usize"))?;
266        acc.checked_add(offset)
267            .ok_or_else(|| anyhow::anyhow!("direct_sum flat offset overflowed usize"))
268    })
269}
270
271fn direct_sum_typed<T: TensorElement + Zero>(
272    a: &IdxTensor,
273    b: &IdxTensor,
274    pairs: &[(DynIndex, DynIndex)],
275) -> Result<(IdxTensor, Vec<DynIndex>)> {
276    if a.tracks_grad() || b.tracks_grad() {
277        return Err(anyhow::anyhow!(
278            "direct_sum does not support tracked tensors until an AD-preserving block path is available"
279        ));
280    }
281    let setup = setup_direct_sum(a, b, pairs)?;
282    let a_data = a.to_vec::<T>()?;
283    let b_data = b.to_vec::<T>()?;
284
285    let mut result_data: Vec<T> = vec![T::zero(); setup.result_total];
286
287    #[allow(clippy::needless_range_loop)]
288    for result_linear in 0..setup.result_total {
289        let result_multi = linear_to_multi(result_linear, &setup.result_dims);
290        let common_multi: Vec<usize> = result_multi[..setup.n_common].to_vec();
291        let paired_multi: Vec<usize> = result_multi[setup.n_common..].to_vec();
292
293        let all_from_a = paired_multi
294            .iter()
295            .enumerate()
296            .all(|(i, &pm)| pm < setup.paired_dims_a[i]);
297        let all_from_b = paired_multi
298            .iter()
299            .enumerate()
300            .all(|(i, &pm)| pm >= setup.paired_dims_a[i]);
301
302        if all_from_a {
303            let a_dims = a.dims();
304            let mut a_multi = vec![0usize; a_dims.len()];
305            for (i, &cp) in setup.common_a_positions.iter().enumerate() {
306                a_multi[cp] = common_multi[i];
307            }
308            for (i, &pp) in setup.paired_a_positions.iter().enumerate() {
309                a_multi[pp] = paired_multi[i];
310            }
311            let a_linear = multi_to_linear(&a_multi, &setup.a_strides)?;
312            result_data[result_linear] = a_data[a_linear];
313        } else if all_from_b {
314            let b_dims = b.dims();
315            let mut b_multi = vec![0usize; b_dims.len()];
316            for (i, &cp) in setup.common_b_positions.iter().enumerate() {
317                b_multi[cp] = common_multi[i];
318            }
319            for (i, &pp) in setup.paired_b_positions.iter().enumerate() {
320                b_multi[pp] = paired_multi[i] - setup.paired_dims_a[i];
321            }
322            let b_linear = multi_to_linear(&b_multi, &setup.b_strides)?;
323            result_data[result_linear] = b_data[b_linear];
324        }
325        // else: mixed case stays T::zero()
326    }
327
328    let result = IdxTensor::from_dense(setup.result_indices, result_data)?;
329    Ok((result, setup.new_indices))
330}
331
332#[cfg(test)]
333mod tests;