Skip to main content

tensor4all_quanticstransform/
cumsum.rs

1//! Cumulative sum operator
2//!
3//! This transformation computes cumulative sums: y_i = Σ_{j < i} x_j
4
5use anyhow::Result;
6use num_complex::Complex64;
7use num_traits::{One, Zero};
8use tensor4all_simplett::{types::tensor3_zeros, Tensor3Ops, TensorTrain};
9
10use crate::common::{tensortrain_to_linear_operator, QuanticsOperator};
11
12/// Type of triangular matrix for cumsum-like operators.
13///
14/// # Variants
15///
16/// - **`Lower`**: Strict lower triangle, M\[i,j\] = 1 when i > j.
17///   Computes the prefix sum: y\_i = sum of x\_j for j < i.
18/// - **`Upper`**: Strict upper triangle, M\[i,j\] = 1 when i < j.
19///   Computes the suffix sum: y\_i = sum of x\_j for j > i.
20///
21/// # Examples
22///
23/// ```
24/// use tensor4all_quanticstransform::{triangle_operator, TriangleType};
25///
26/// // Prefix sum (lower triangle)
27/// let lower = triangle_operator(4, TriangleType::Lower).unwrap();
28/// assert_eq!(lower.mpo.node_count(), 4);
29///
30/// // Suffix sum (upper triangle)
31/// let upper = triangle_operator(4, TriangleType::Upper).unwrap();
32/// assert_eq!(upper.mpo.node_count(), 4);
33/// ```
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum TriangleType {
36    /// Strict lower triangle: M\[i,j\] = 1 when i > j.
37    /// Computes y\_i = sum of x\_j for j < i (prefix sum).
38    Lower,
39    /// Strict upper triangle: M\[i,j\] = 1 when i < j.
40    /// Computes y\_i = sum of x\_j for j > i (suffix sum).
41    Upper,
42}
43
44/// Create a cumulative sum operator: y_i = Σ_{j < i} x_j
45///
46/// This MPO implements a strict lower triangular matrix filled with ones.
47/// For a function g defined on {0, 1, ..., 2^R - 1}, it computes:
48/// f(i) = Σ_{j < i} g(j)
49///
50/// # Arguments
51/// * `r` - Number of bits (sites). Must be at least 2.
52///
53/// # Returns
54/// LinearOperator representing the cumulative sum
55///
56/// # Examples
57///
58/// ```
59/// use tensor4all_quanticstransform::cumsum_operator;
60///
61/// let op = cumsum_operator(4).unwrap();
62/// assert_eq!(op.mpo.node_count(), 4);
63///
64/// // Requires at least 2 sites
65/// assert!(cumsum_operator(1).is_err());
66/// assert!(cumsum_operator(0).is_err());
67/// ```
68pub fn cumsum_operator(r: usize) -> Result<QuanticsOperator> {
69    if r < 2 {
70        anyhow::bail!("Number of sites must be at least 2, got {r}");
71    }
72
73    let mpo = cumsum_mpo(r)?;
74    let site_dims = vec![2; r];
75    tensortrain_to_linear_operator(&mpo, &site_dims)
76}
77
78/// Create a triangular matrix operator with the specified triangle type.
79///
80/// - `Lower`: M\[i,j\] = 1 when i > j (prefix sum: y\_i = sum of x\_j for j < i)
81/// - `Upper`: M\[i,j\] = 1 when i < j (suffix sum: y\_i = sum of x\_j for j > i)
82///
83/// # Arguments
84/// * `r` - Number of bits (sites). Must be at least 2.
85/// * `triangle` - Which triangle to use
86///
87/// # Examples
88///
89/// ```
90/// use tensor4all_quanticstransform::{triangle_operator, TriangleType};
91///
92/// let op = triangle_operator(4, TriangleType::Lower).unwrap();
93/// assert_eq!(op.mpo.node_count(), 4);
94///
95/// // Requires at least 2 sites
96/// assert!(triangle_operator(1, TriangleType::Lower).is_err());
97/// ```
98pub fn triangle_operator(r: usize, triangle: TriangleType) -> Result<QuanticsOperator> {
99    if r < 2 {
100        anyhow::bail!("Number of sites must be at least 2, got {r}");
101    }
102
103    let mpo = triangle_mpo(r, triangle)?;
104    let site_dims = vec![2; r];
105    tensortrain_to_linear_operator(&mpo, &site_dims)
106}
107
108/// Create the cumulative sum MPO as a TensorTrain.
109///
110/// The cumulative sum is implemented as an upper triangular matrix.
111/// The MPO tracks whether a comparison has been made:
112/// - State 0: No comparison yet (y and x equal so far)
113/// - State 1: Comparison made (y > x, so this entry is 1)
114///
115/// Tensor entries t[left, right, y, x]:
116/// - t[0, 0, y, x] = 1 if y == x (both 0 or both 1)
117/// - t[0, 1, 1, 0] = 1 (y > x at this position)
118/// - t[1, 1, *, *] = 1 (comparison already made)
119#[allow(clippy::needless_range_loop)]
120fn cumsum_mpo(r: usize) -> Result<TensorTrain<Complex64>> {
121    if r < 2 {
122        anyhow::bail!("Number of sites must be at least 2, got {r}");
123    }
124
125    let single_tensor = upper_triangle_tensor();
126    let mut tensors = Vec::with_capacity(r);
127
128    for n in 0..r {
129        if n == 0 {
130            // First tensor: start in state 0 (no comparison yet)
131            // Contract with [1, 0] on left link to select state 0
132            let mut t = tensor3_zeros(1, 4, 2);
133            for cout in 0..2 {
134                for y_bit in 0..2 {
135                    for x_bit in 0..2 {
136                        let val = single_tensor[0][cout][y_bit][x_bit];
137                        let s = y_bit * 2 + x_bit;
138                        t.set3(0, s, cout, val);
139                    }
140                }
141            }
142            tensors.push(t);
143        } else if n == r - 1 {
144            // Last tensor: select entries where state is 1 (y > x)
145            // The output is 1 only if comparison was made (state 1)
146            // For upper triangle (strict), diagonal is excluded
147            let mut t = tensor3_zeros(2, 4, 1);
148            for cin in 0..2 {
149                for y_bit in 0..2 {
150                    for x_bit in 0..2 {
151                        // Sum over cout states, weighted by whether comparison was made
152                        let mut sum = Complex64::zero();
153                        for cout in 0..2 {
154                            let val = single_tensor[cin][cout][y_bit][x_bit];
155                            // Only count if we end in state 1 (comparison made)
156                            if cout == 1 {
157                                sum += val;
158                            }
159                        }
160                        let s = y_bit * 2 + x_bit;
161                        t.set3(cin, s, 0, sum);
162                    }
163                }
164            }
165            tensors.push(t);
166        } else {
167            // Middle tensors: full tensor
168            let mut t = tensor3_zeros(2, 4, 2);
169            for cin in 0..2 {
170                for cout in 0..2 {
171                    for y_bit in 0..2 {
172                        for x_bit in 0..2 {
173                            let val = single_tensor[cin][cout][y_bit][x_bit];
174                            let s = y_bit * 2 + x_bit;
175                            t.set3(cin, s, cout, val);
176                        }
177                    }
178                }
179            }
180            tensors.push(t);
181        }
182    }
183
184    TensorTrain::new(tensors).map_err(|e| anyhow::anyhow!("Failed to create cumsum MPO: {}", e))
185}
186
187/// Create a triangular matrix MPO as a TensorTrain.
188///
189/// This is a generalization of `cumsum_mpo` that supports both upper and lower triangles.
190#[allow(clippy::needless_range_loop)]
191fn triangle_mpo(r: usize, triangle: TriangleType) -> Result<TensorTrain<Complex64>> {
192    if r < 2 {
193        anyhow::bail!("Number of sites must be at least 2, got {r}");
194    }
195
196    // upper_triangle_tensor() has y>x transition → M[i,j]=1 when i>j = Lower triangle
197    // lower_triangle_tensor() has y<x transition → M[i,j]=1 when i<j = Upper triangle
198    let single_tensor = match triangle {
199        TriangleType::Lower => upper_triangle_tensor(),
200        TriangleType::Upper => lower_triangle_tensor(),
201    };
202    let mut tensors = Vec::with_capacity(r);
203
204    for n in 0..r {
205        if n == 0 {
206            let mut t = tensor3_zeros(1, 4, 2);
207            for cout in 0..2 {
208                for y_bit in 0..2 {
209                    for x_bit in 0..2 {
210                        let val = single_tensor[0][cout][y_bit][x_bit];
211                        let s = y_bit * 2 + x_bit;
212                        t.set3(0, s, cout, val);
213                    }
214                }
215            }
216            tensors.push(t);
217        } else if n == r - 1 {
218            let mut t = tensor3_zeros(2, 4, 1);
219            for cin in 0..2 {
220                for y_bit in 0..2 {
221                    for x_bit in 0..2 {
222                        let mut sum = Complex64::zero();
223                        for cout in 0..2 {
224                            let val = single_tensor[cin][cout][y_bit][x_bit];
225                            if cout == 1 {
226                                sum += val;
227                            }
228                        }
229                        let s = y_bit * 2 + x_bit;
230                        t.set3(cin, s, 0, sum);
231                    }
232                }
233            }
234            tensors.push(t);
235        } else {
236            let mut t = tensor3_zeros(2, 4, 2);
237            for cin in 0..2 {
238                for cout in 0..2 {
239                    for y_bit in 0..2 {
240                        for x_bit in 0..2 {
241                            let val = single_tensor[cin][cout][y_bit][x_bit];
242                            let s = y_bit * 2 + x_bit;
243                            t.set3(cin, s, cout, val);
244                        }
245                    }
246                }
247            }
248            tensors.push(t);
249        }
250    }
251
252    TensorTrain::new(tensors).map_err(|e| anyhow::anyhow!("Failed to create triangle MPO: {}", e))
253}
254
255/// Create the single-site tensor for upper triangular matrix.
256///
257/// Returns a 4D tensor [cin][cout][y_bit][x_bit] where:
258/// - cin: input state (0 = no comparison yet, 1 = comparison made)
259/// - cout: output state
260/// - y_bit: output (row) bit
261/// - x_bit: input (column) bit
262///
263/// The tensor implements strict upper triangle comparison:
264/// - State 0: Comparing bits. If y > x, transition to state 1.
265/// - State 1: Comparison made. All remaining entries are 1.
266fn upper_triangle_tensor() -> [[[[Complex64; 2]; 2]; 2]; 2] {
267    let mut tensor = [[[[Complex64::zero(); 2]; 2]; 2]; 2];
268
269    // State 0 -> State 0: y == x (continue comparing)
270    tensor[0][0][0][0] = Complex64::one(); // y=0, x=0
271    tensor[0][0][1][1] = Complex64::one(); // y=1, x=1
272
273    // State 0 -> State 1: y > x (comparison made, y is greater)
274    tensor[0][1][1][0] = Complex64::one(); // y=1, x=0 (y > x at this bit)
275
276    // State 0 -> nowhere: y < x (this entry is 0, not in upper triangle)
277    // tensor[0][*][0][1] = 0 (implicit)
278
279    // State 1 -> State 1: Comparison already made, all entries are 1
280    tensor[1][1][0][0] = Complex64::one();
281    tensor[1][1][0][1] = Complex64::one();
282    tensor[1][1][1][0] = Complex64::one();
283    tensor[1][1][1][1] = Complex64::one();
284
285    tensor
286}
287
288/// Create the single-site tensor for strict upper triangle (i < j).
289///
290/// M[i,j] = 1 when i < j (suffix sum: y_i = Σ_{j > i} x_j).
291///
292/// The tensor is identical to lower_triangle but with the transition
293/// on y < x (y=0, x=1) instead of y > x (y=1, x=0).
294fn lower_triangle_tensor() -> [[[[Complex64; 2]; 2]; 2]; 2] {
295    let mut tensor = [[[[Complex64::zero(); 2]; 2]; 2]; 2];
296
297    // State 0 -> State 0: y == x (continue comparing)
298    tensor[0][0][0][0] = Complex64::one(); // y=0, x=0
299    tensor[0][0][1][1] = Complex64::one(); // y=1, x=1
300
301    // State 0 -> State 1: y < x (comparison made, x is greater)
302    tensor[0][1][0][1] = Complex64::one(); // y=0, x=1 (y < x at this bit)
303
304    // State 1 -> State 1: Comparison already made, all entries are 1
305    tensor[1][1][0][0] = Complex64::one();
306    tensor[1][1][0][1] = Complex64::one();
307    tensor[1][1][1][0] = Complex64::one();
308    tensor[1][1][1][1] = Complex64::one();
309
310    tensor
311}
312
313#[cfg(test)]
314mod tests;