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