tensor4all_core/defaults/
direct_sum.rs1use 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
17pub 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
80struct 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 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 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 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 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 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 let mut result_indices: Vec<DynIndex> = Vec::new();
193 let mut result_dims: Vec<usize> = Vec::new();
194
195 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 for new_idx in &new_indices {
204 result_indices.push(new_idx.clone());
205 result_dims.push(new_idx.dim());
206 }
207
208 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 }
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;